summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2018-05-26 12:35:14 +0900
committerGitHub <noreply@github.com>2018-05-26 12:35:14 +0900
commitbc9487822529b260fbdc9a8dcca35c3da25db04e (patch)
treed5fd0dffb95e03b95df8e98692305d75f855e645 /src
parentNew translations ja.yml (Portuguese) (diff)
parentMerge pull request #1649 from l2dy/i18n (diff)
downloadmisskey-bc9487822529b260fbdc9a8dcca35c3da25db04e.tar.gz
misskey-bc9487822529b260fbdc9a8dcca35c3da25db04e.tar.bz2
misskey-bc9487822529b260fbdc9a8dcca35c3da25db04e.zip
Merge branch 'master' into l10n_master
Diffstat (limited to 'src')
-rw-r--r--src/build/i18n.ts11
-rw-r--r--src/client/app/app.styl5
-rw-r--r--src/client/app/boot.js34
-rw-r--r--src/client/app/common/scripts/check-for-update.ts2
-rw-r--r--src/client/app/common/scripts/streaming/home.ts2
-rw-r--r--src/client/app/common/views/components/avatar.vue11
-rw-r--r--src/client/app/common/views/components/connect-failed.troubleshooter.vue6
-rw-r--r--src/client/app/common/views/components/connect-failed.vue8
-rw-r--r--src/client/app/common/views/components/messaging-room.form.vue12
-rw-r--r--src/client/app/common/views/components/messaging-room.message.vue28
-rw-r--r--src/client/app/common/views/components/messaging-room.vue86
-rw-r--r--src/client/app/common/views/components/othello.game.vue8
-rw-r--r--src/client/app/common/views/components/poll-editor.vue2
-rw-r--r--src/client/app/common/views/components/poll.vue8
-rw-r--r--src/client/app/common/views/components/reaction-picker.vue2
-rw-r--r--src/client/app/common/views/components/signin.vue2
-rw-r--r--src/client/app/common/views/components/signup.vue2
-rw-r--r--src/client/app/common/views/components/time.vue20
-rw-r--r--src/client/app/common/views/components/twitter-setting.vue2
-rw-r--r--src/client/app/common/views/components/visibility-chooser.vue16
-rw-r--r--src/client/app/common/views/components/welcome-timeline.vue21
-rw-r--r--src/client/app/common/views/widgets/broadcast.vue2
-rw-r--r--src/client/app/common/views/widgets/donation.vue4
-rw-r--r--src/client/app/config.ts2
-rw-r--r--src/client/app/desktop/views/components/calendar.vue18
-rw-r--r--src/client/app/desktop/views/components/drive.file.vue26
-rw-r--r--src/client/app/desktop/views/components/drive.folder.vue20
-rw-r--r--src/client/app/desktop/views/components/drive.nav-folder.vue2
-rw-r--r--src/client/app/desktop/views/components/drive.vue26
-rw-r--r--src/client/app/desktop/views/components/followers-window.vue9
-rw-r--r--src/client/app/desktop/views/components/following-window.vue9
-rw-r--r--src/client/app/desktop/views/components/home.vue2
-rw-r--r--src/client/app/desktop/views/components/note-detail.vue51
-rw-r--r--src/client/app/desktop/views/components/notes.note.sub.vue18
-rw-r--r--src/client/app/desktop/views/components/notes.note.vue14
-rw-r--r--src/client/app/desktop/views/components/notes.vue4
-rw-r--r--src/client/app/desktop/views/components/notifications.vue2
-rw-r--r--src/client/app/desktop/views/components/post-form-window.vue4
-rw-r--r--src/client/app/desktop/views/components/post-form.vue26
-rw-r--r--src/client/app/desktop/views/components/renote-form.vue6
-rw-r--r--src/client/app/desktop/views/components/settings-window.vue2
-rw-r--r--src/client/app/desktop/views/components/settings.2fa.vue10
-rw-r--r--src/client/app/desktop/views/components/settings.api.vue2
-rw-r--r--src/client/app/desktop/views/components/settings.password.vue10
-rw-r--r--src/client/app/desktop/views/components/settings.profile.vue8
-rw-r--r--src/client/app/desktop/views/components/settings.vue110
-rw-r--r--src/client/app/desktop/views/components/sub-note-content.vue6
-rw-r--r--src/client/app/desktop/views/components/ui.header.account.vue7
-rw-r--r--src/client/app/desktop/views/components/user-lists-window.vue2
-rw-r--r--src/client/app/desktop/views/components/users-list.item.vue2
-rw-r--r--src/client/app/desktop/views/components/window.vue6
-rw-r--r--src/client/app/desktop/views/pages/drive.vue7
-rw-r--r--src/client/app/desktop/views/pages/favorites.vue2
-rw-r--r--src/client/app/desktop/views/pages/home-customize.vue2
-rw-r--r--src/client/app/desktop/views/pages/messaging-room.vue16
-rw-r--r--src/client/app/desktop/views/pages/selectdrive.vue2
-rw-r--r--src/client/app/desktop/views/pages/user-list.users.vue6
-rw-r--r--src/client/app/desktop/views/pages/user/user.timeline.vue8
-rw-r--r--src/client/app/desktop/views/pages/welcome.vue286
-rw-r--r--src/client/app/desktop/views/widgets/polls.vue2
-rw-r--r--src/client/app/desktop/views/widgets/profile.vue4
-rw-r--r--src/client/app/init.ts87
-rw-r--r--src/client/app/mios.ts18
-rw-r--r--src/client/app/mobile/script.ts10
-rw-r--r--src/client/app/mobile/views/components/drive.vue2
-rw-r--r--src/client/app/mobile/views/components/follow-button.vue2
-rw-r--r--src/client/app/mobile/views/components/media-image.vue10
-rw-r--r--src/client/app/mobile/views/components/note-detail.vue32
-rw-r--r--src/client/app/mobile/views/components/note-preview.vue37
-rw-r--r--src/client/app/mobile/views/components/note.sub.vue36
-rw-r--r--src/client/app/mobile/views/components/note.vue48
-rw-r--r--src/client/app/mobile/views/components/notifications.vue2
-rw-r--r--src/client/app/mobile/views/components/post-form.vue2
-rw-r--r--src/client/app/mobile/views/components/ui.nav.vue11
-rw-r--r--src/client/app/mobile/views/components/user-card.vue21
-rw-r--r--src/client/app/mobile/views/components/user-timeline.vue2
-rw-r--r--src/client/app/mobile/views/pages/followers.vue4
-rw-r--r--src/client/app/mobile/views/pages/following.vue4
-rw-r--r--src/client/app/mobile/views/pages/home.vue14
-rw-r--r--src/client/app/mobile/views/pages/messaging-room.vue19
-rw-r--r--src/client/app/mobile/views/pages/messaging.vue1
-rw-r--r--src/client/app/mobile/views/pages/notifications.vue2
-rw-r--r--src/client/app/mobile/views/pages/profile-setting.vue225
-rw-r--r--src/client/app/mobile/views/pages/search.vue2
-rw-r--r--src/client/app/mobile/views/pages/selectdrive.vue2
-rw-r--r--src/client/app/mobile/views/pages/settings.vue270
-rw-r--r--src/client/app/mobile/views/pages/settings/settings.profile.vue151
-rw-r--r--src/client/app/mobile/views/pages/user.vue2
-rw-r--r--src/client/app/mobile/views/pages/widgets.vue2
-rw-r--r--src/client/app/reset.styl3
-rw-r--r--src/client/app/store.ts65
-rw-r--r--src/client/assets/title.svg25
-rw-r--r--src/client/assets/version.html5
-rw-r--r--src/client/md.scss2
-rw-r--r--src/config/types.ts2
-rw-r--r--src/index.ts9
-rw-r--r--src/models/drive-file.ts5
-rw-r--r--src/models/note.ts4
-rw-r--r--src/models/user.ts1
-rw-r--r--src/server/api/common/read-notification.ts9
-rw-r--r--src/server/api/endpoints.ts2
-rw-r--r--src/server/api/endpoints/drive/files/create.ts9
-rw-r--r--src/server/api/endpoints/i/notifications.ts3
-rw-r--r--src/server/api/endpoints/i/update.ts6
-rw-r--r--src/server/api/endpoints/notes.ts8
-rw-r--r--src/server/api/endpoints/notes/conversation.ts (renamed from src/server/api/endpoints/notes/context.ts)15
-rw-r--r--src/server/api/endpoints/notifications/get_unread_count.ts3
-rw-r--r--src/server/file/send-drive-file.ts11
-rw-r--r--src/server/index.ts6
-rw-r--r--src/server/web/index.ts4
-rw-r--r--src/services/drive/add-file.ts444
-rw-r--r--src/services/drive/upload-from-url.ts13
-rw-r--r--src/services/note/create.ts2
113 files changed, 1385 insertions, 1322 deletions
diff --git a/src/build/i18n.ts b/src/build/i18n.ts
index addc35ce59..35854055d0 100644
--- a/src/build/i18n.ts
+++ b/src/build/i18n.ts
@@ -7,7 +7,7 @@ import locale from '../../locales';
export default class Replacer {
private lang: string;
- public pattern = /%i18n:([a-z0-9_\-\.\/\|\!]+?)%/g;
+ public pattern = /%i18n:([a-z0-9_\-\.\/\|]+?)%/g;
constructor(lang: string) {
this.lang = lang;
@@ -56,11 +56,6 @@ export default class Replacer {
public replacement(match, key) {
let path = null;
- const shouldEscape = key[0] == '!';
- if (shouldEscape) {
- key = key.substr(1);
- }
-
if (key.indexOf('|') != -1) {
path = key.split('|')[0];
key = key.split('|')[1];
@@ -68,8 +63,6 @@ export default class Replacer {
const txt = this.get(path, key);
- return shouldEscape
- ? txt.replace(/'/g, '\\x27').replace(/"/g, '\\x22')
- : txt.replace(/"/g, '&quot;');
+ return txt.replace(/'/g, '\\x27').replace(/"/g, '\\x22');
}
}
diff --git a/src/client/app/app.styl b/src/client/app/app.styl
index 431b9daa65..ba694b73ae 100644
--- a/src/client/app/app.styl
+++ b/src/client/app/app.styl
@@ -7,6 +7,11 @@ html
cursor progress !important
body
+ // for md
+ font-size 16px !important
+ line-height initial !important
+ letter-spacing initial !important
+
overflow-wrap break-word
#error
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 9338bc501e..7b884c8a54 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -18,6 +18,14 @@
return;
}
+ //#region Load settings
+ let settings = null;
+ const vuex = localStorage.getItem('vuex');
+ if (vuex) {
+ settings = JSON.parse(vuex);
+ }
+ //#endregion
+
// Get the current url information
const url = new URL(location.href);
@@ -29,11 +37,16 @@
if (url.pathname == '/auth') app = 'auth';
//#endregion
- // Detect the user language
- // Note: The default language is Japanese
+ //#region Detect the user language
let lang = navigator.language.split('-')[0];
+
+ // The default language is English
if (!LANGS.includes(lang)) lang = 'en';
- if (localStorage.getItem('lang')) lang = localStorage.getItem('lang');
+
+ if (settings) {
+ if (settings.device.lang) lang = settings.device.lang;
+ }
+ //#endregion
// Detect the user agent
const ua = navigator.userAgent.toLowerCase();
@@ -61,20 +74,15 @@
}
// Dark/Light
- if (localStorage.getItem('darkmode') == 'true') {
- document.documentElement.setAttribute('data-darkmode', 'true');
+ if (settings) {
+ if (settings.device.darkmode) {
+ document.documentElement.setAttribute('data-darkmode', 'true');
+ }
}
// Script version
const ver = localStorage.getItem('v') || VERSION;
- // Whether in debug mode
- const isDebug = localStorage.getItem('debug') == 'true';
-
- // Whether use raw version script
- const raw = (localStorage.getItem('useRawScript') == 'true' && isDebug)
- || ENV != 'production';
-
// Get salt query
const salt = localStorage.getItem('salt')
? '?salt=' + localStorage.getItem('salt')
@@ -84,7 +92,7 @@
// Note: 'async' make it possible to load the script asyncly.
// 'defer' make it possible to run the script when the dom loaded.
const script = document.createElement('script');
- script.setAttribute('src', `/assets/${app}.${ver}.${lang}.${raw ? 'raw' : 'min'}.js${salt}`);
+ script.setAttribute('src', `/assets/${app}.${ver}.${lang}.js${salt}`);
script.setAttribute('async', 'true');
script.setAttribute('defer', 'true');
head.appendChild(script);
diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts
index 1e303017eb..b5ba6916d1 100644
--- a/src/client/app/common/scripts/check-for-update.ts
+++ b/src/client/app/common/scripts/check-for-update.ts
@@ -23,7 +23,7 @@ export default async function(mios: MiOS, force = false, silent = false) {
}
if (!silent) {
- alert('%i18n:!common.update-available%'.replace('{newer}', newer).replace('{current}', current));
+ alert('%i18n:common.update-available%'.replace('{newer}', newer).replace('{current}', current));
}
return newer;
diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts
index 09d830bece..44d07e331a 100644
--- a/src/client/app/common/scripts/streaming/home.ts
+++ b/src/client/app/common/scripts/streaming/home.ts
@@ -62,7 +62,7 @@ export class HomeStream extends Stream {
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
this.on('my_token_regenerated', () => {
- alert('%i18n:!common.my-token-regenerated%');
+ alert('%i18n:common.my-token-regenerated%');
os.signout();
});
}
diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue
index 8ec359e83c..3e1b17635f 100644
--- a/src/client/app/common/views/components/avatar.vue
+++ b/src/client/app/common/views/components/avatar.vue
@@ -21,10 +21,17 @@ export default Vue.extend({
}
},
computed: {
+ lightmode(): boolean {
+ return this.$store.state.device.lightmode;
+ },
style(): any {
return {
- backgroundColor: this.user.avatarColor && this.user.avatarColor.length == 3 ? `rgb(${ this.user.avatarColor.join(',') })` : null,
- backgroundImage: `url(${ this.user.avatarUrl }?thumbnail)`,
+ backgroundColor: this.lightmode
+ ? `rgb(${ this.user.avatarColor.slice(0, 3).join(',') })`
+ : this.user.avatarColor && this.user.avatarColor.length == 3
+ ? `rgb(${ this.user.avatarColor.join(',') })`
+ : null,
+ backgroundImage: this.lightmode ? null : `url(${ this.user.avatarUrl }?thumbnail)`,
borderRadius: (this as any).clientSettings.circleIcons ? '100%' : null
};
}
diff --git a/src/client/app/common/views/components/connect-failed.troubleshooter.vue b/src/client/app/common/views/components/connect-failed.troubleshooter.vue
index 6a922676b7..6c23cc7969 100644
--- a/src/client/app/common/views/components/connect-failed.troubleshooter.vue
+++ b/src/client/app/common/views/components/connect-failed.troubleshooter.vue
@@ -8,21 +8,21 @@
<template v-if="network">%fa:check%</template>
<template v-if="!network">%fa:times%</template>
</template>
- {{ network == null ? '%i18n:!@checking-network%' : '%i18n:!@network%' }}<mk-ellipsis v-if="network == null"/>
+ {{ network == null ? '%i18n:@checking-network%' : '%i18n:@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:!@checking-internet%' : '%i18n:!@internet%' }}<mk-ellipsis v-if="internet == null"/>
+ {{ internet == null ? '%i18n:@checking-internet%' : '%i18n:@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:!@checking-server%' : '%i18n:!@server%' }}<mk-ellipsis v-if="server == null"/>
+ {{ server == null ? '%i18n:@checking-server%' : '%i18n:@server%' }}<mk-ellipsis v-if="server == null"/>
</p>
</div>
<p v-if="!end">%i18n:@finding%<mk-ellipsis/></p>
diff --git a/src/client/app/common/views/components/connect-failed.vue b/src/client/app/common/views/components/connect-failed.vue
index 6c194ff982..0f686926b0 100644
--- a/src/client/app/common/views/components/connect-failed.vue
+++ b/src/client/app/common/views/components/connect-failed.vue
@@ -3,9 +3,9 @@
<img src="data:image/jpeg;base64,%base64:/assets/error.jpg%" alt=""/>
<h1>%i18n:@title%</h1>
<p class="text">
- <span>{{ '%i18n:!@description%'.substr(0, '%i18n:!@description%'.indexOf('{')) }}</span>
- <a @click="reload">{{ '%i18n:!@description%'.match(/\{(.+?)\}/)[1] }}</a>
- <span>{{ '%i18n:!@description%'.substr('%i18n:!@description%'.indexOf('}') + 1) }}</span>
+ <span>{{ '%i18n:@description%'.substr(0, '%i18n:@description%'.indexOf('{')) }}</span>
+ <a @click="reload">{{ '%i18n:@description%'.match(/\{(.+?)\}/)[1] }}</a>
+ <span>{{ '%i18n:@description%'.substr('%i18n:@description%'.indexOf('}') + 1) }}</span>
</p>
<button v-if="!troubleshooting" @click="troubleshooting = true">%i18n:@troubleshoot%</button>
<x-troubleshooter v-if="troubleshooting"/>
@@ -28,7 +28,7 @@ export default Vue.extend({
},
mounted() {
document.title = 'Oops!';
- document.documentElement.style.background = '#f8f8f8';
+ document.documentElement.style.setProperty('background', '#f8f8f8', 'important');
},
methods: {
reload() {
diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue
index 32a43ace57..050906cf44 100644
--- a/src/client/app/common/views/components/messaging-room.form.vue
+++ b/src/client/app/common/views/components/messaging-room.form.vue
@@ -197,7 +197,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-messaging-form
+root(isDark)
> textarea
cursor auto
display block
@@ -209,10 +209,10 @@ export default Vue.extend({
padding 8px
resize none
font-size 1em
- color #000
+ color isDark ? #fff : #000
outline none
border none
- border-top solid 1px #eee
+ border-top solid 1px isDark ? #4b5056 : #eee
border-radius 0
box-shadow none
background transparent
@@ -302,4 +302,10 @@ export default Vue.extend({
input[type=file]
display none
+.mk-messaging-form[data-darkmode]
+ root(true)
+
+.mk-messaging-form:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index ba0ab3209f..ef39199dc4 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -59,8 +59,10 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.message
- $me-balloon-color = #23A7B6
+@import '~const.styl'
+
+root(isDark)
+ $me-balloon-color = $theme-color
padding 10px 12px 10px 12px
background-color transparent
@@ -126,7 +128,7 @@ export default Vue.extend({
bottom -4px
left -12px
margin 0
- color rgba(#000, 0.5)
+ color isDark ? rgba(#fff, 0.5) : rgba(#000, 0.5)
font-size 11px
> .content
@@ -187,7 +189,7 @@ export default Vue.extend({
display block
margin 2px 0 0 0
font-size 10px
- color rgba(#000, 0.4)
+ color isDark ? rgba(#fff, 0.4) : rgba(#000, 0.4)
> [data-fa]
margin-left 4px
@@ -200,8 +202,9 @@ export default Vue.extend({
padding-left 66px
> .balloon
+ $color = isDark ? #2d3338 : #eee
float left
- background #eee
+ background $color
&[data-no-text]
background transparent
@@ -209,10 +212,15 @@ export default Vue.extend({
&:not([data-no-text]):before
left -14px
border-top solid 8px transparent
- border-right solid 8px #eee
+ border-right solid 8px $color
border-bottom solid 8px transparent
border-left solid 8px transparent
+ > .content
+ > .text
+ if isDark
+ color #fff
+
> footer
text-align left
@@ -241,7 +249,7 @@ export default Vue.extend({
> .content
> p.is-deleted
- color rgba(255, 255, 255, 0.5)
+ color rgba(#fff, 0.5)
> .text >>>
&, *
@@ -254,4 +262,10 @@ export default Vue.extend({
> .baloon
opacity 0.5
+.message[data-darkmode]
+ root(true)
+
+.message:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue
index a45114e6bb..79756b22eb 100644
--- a/src/client/app/common/views/components/messaging-room.vue
+++ b/src/client/app/common/views/components/messaging-room.vue
@@ -8,7 +8,7 @@
<p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:@empty%</p>
<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages">%fa:flag%%i18n:@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:!@more%' }}
+ <template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:@more%' }}
</button>
<template v-for="(message, i) in _messages">
<x-message :message="message" :key="message.id"/>
@@ -18,7 +18,11 @@
</template>
</div>
<footer>
- <div ref="notifications" class="notifications"></div>
+ <transition name="fade">
+ <div class="new-message" v-show="showIndicator">
+ <button @click="onIndicatorClick">%fa:arrow-circle-down%%i18n:@new-message%</button>
+ </div>
+ </transition>
<x-form :user="user" ref="form"/>
</footer>
</div>
@@ -45,7 +49,9 @@ export default Vue.extend({
fetchingMoreMessages: false,
messages: [],
existMoreMessages: false,
- connection: null
+ connection: null,
+ showIndicator: false,
+ timer: null
};
},
@@ -149,9 +155,9 @@ export default Vue.extend({
onMessage(message) {
// サウンドを再生する
- if ((this as any).os.isEnableSounds) {
+ if (this.$store.state.device.enableSounds) {
const sound = new Audio(`${url}/assets/message.mp3`);
- sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+ sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
@@ -172,7 +178,7 @@ export default Vue.extend({
});
} else if (message.userId != (this as any).os.i.id) {
// Notify
- this.notify('%i18n:!@new-message%');
+ this.notifyNewMessage();
}
},
@@ -205,18 +211,18 @@ export default Vue.extend({
}
},
- 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);
+ onIndicatorClick() {
+ this.showIndicator = false;
+ this.scrollToBottom();
+ },
+
+ notifyNewMessage() {
+ this.showIndicator = true;
- setTimeout(() => {
- n.style.opacity = 0;
- setTimeout(() => n.parentNode.removeChild(n), 1000);
+ if (this.timer) clearTimeout(this.timer);
+
+ this.timer = setTimeout(() => {
+ this.showIndicator = false;
}, 4000);
},
@@ -238,11 +244,12 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-messaging-room
+root(isDark)
display flex
flex 1
flex-direction column
height 100%
+ background isDark ? #191b22 : #fff
> .stream
width 100%
@@ -256,7 +263,7 @@ export default Vue.extend({
padding 16px 8px 8px 8px
text-align center
font-size 0.8em
- color rgba(#000, 0.4)
+ color rgba(isDark ? #fff : #000, 0.4)
[data-fa]
margin-right 4px
@@ -267,7 +274,7 @@ export default Vue.extend({
padding 16px 8px 8px 8px
text-align center
font-size 0.8em
- color rgba(#000, 0.4)
+ color rgba(isDark ? #fff : #000, 0.4)
[data-fa]
margin-right 4px
@@ -278,7 +285,7 @@ export default Vue.extend({
padding 16px
text-align center
font-size 0.8em
- color rgba(#000, 0.4)
+ color rgba(isDark ? #fff : #000, 0.4)
[data-fa]
margin-right 4px
@@ -322,7 +329,7 @@ export default Vue.extend({
left 0
right 0
margin 0 auto
- background rgba(#000, 0.1)
+ background rgba(isDark ? #fff : #000, 0.1)
> span
display inline-block
@@ -330,8 +337,8 @@ export default Vue.extend({
padding 0 16px
//font-weight bold
line-height 32px
- color rgba(#000, 0.3)
- background #fff
+ color rgba(isDark ? #fff : #000, 0.3)
+ background isDark ? #191b22 : #fff
> footer
position -webkit-sticky
@@ -342,30 +349,32 @@ export default Vue.extend({
max-width 600px
margin 0 auto
padding 0
- background rgba(255, 255, 255, 0.95)
+ background rgba(isDark ? #282c37 : #fff, 0.95)
background-clip content-box
- > .notifications
+ > .new-message
position absolute
top -48px
width 100%
padding 8px 0
text-align center
- &:empty
- display none
-
- > p
+ > button
display inline-block
margin 0
- padding 0 12px 0 28px
+ padding 0 12px 0 30px
cursor pointer
line-height 32px
font-size 12px
color $theme-color-foreground
background $theme-color
border-radius 16px
- transition opacity 1s ease
+
+ &:hover
+ background lighten($theme-color, 10%)
+
+ &:active
+ background darken($theme-color, 10%)
> [data-fa]
position absolute
@@ -374,4 +383,17 @@ export default Vue.extend({
line-height 32px
font-size 16px
+.fade-enter-active, .fade-leave-active
+ transition opacity 0.1s
+
+.fade-enter, .fade-leave-to
+ transition opacity 0.5s
+ opacity 0
+
+.mk-messaging-room[data-darkmode]
+ root(true)
+
+.mk-messaging-room:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/common/views/components/othello.game.vue b/src/client/app/common/views/components/othello.game.vue
index 8c646cce07..ea75558d10 100644
--- a/src/client/app/common/views/components/othello.game.vue
+++ b/src/client/app/common/views/components/othello.game.vue
@@ -162,9 +162,9 @@ export default Vue.extend({
this.o.put(this.myColor, pos);
// サウンドを再生する
- if ((this as any).os.isEnableSounds) {
+ if (this.$store.state.device.enableSounds) {
const sound = new Audio(`${url}/assets/othello-put-me.mp3`);
- sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+ sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
@@ -186,9 +186,9 @@ export default Vue.extend({
this.$forceUpdate();
// サウンドを再生する
- if ((this as any).os.isEnableSounds && x.color != this.myColor) {
+ if (this.$store.state.device.enableSounds && x.color != this.myColor) {
const sound = new Audio(`${url}/assets/othello-put-you.mp3`);
- sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+ sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
},
diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue
index 95bcba996e..115c934c8b 100644
--- a/src/client/app/common/views/components/poll-editor.vue
+++ b/src/client/app/common/views/components/poll-editor.vue
@@ -5,7 +5,7 @@
</p>
<ul ref="choices">
<li v-for="(choice, i) in choices">
- <input :value="choice" @input="onInput(i, $event)" :placeholder="'%i18n:!@choice-n%'.replace('{}', i + 1)">
+ <input :value="choice" @input="onInput(i, $event)" :placeholder="'%i18n:@choice-n%'.replace('{}', i + 1)">
<button @click="remove(i)" title="%i18n:@remove%">
%fa:times%
</button>
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
index 46e41cbcdb..660247edbc 100644
--- a/src/client/app/common/views/components/poll.vue
+++ b/src/client/app/common/views/components/poll.vue
@@ -1,19 +1,19 @@
<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:!@vote-to%'.replace('{}', choice.text) : ''">
+ <li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? '%i18n:@vote-to%'.replace('{}', choice.text) : ''">
<div class="backdrop" :style="{ 'width': (showResult ? (choice.votes / total * 100) : 0) + '%' }"></div>
<span>
<template v-if="choice.isVoted">%fa:check%</template>
<span>{{ choice.text }}</span>
- <span class="votes" v-if="showResult">({{ '%i18n:!@vote-count%'.replace('{}', choice.votes) }})</span>
+ <span class="votes" v-if="showResult">({{ '%i18n:@vote-count%'.replace('{}', choice.votes) }})</span>
</span>
</li>
</ul>
<p v-if="total > 0">
- <span>{{ '%i18n:!@total-users%'.replace('{}', total) }}</span>
+ <span>{{ '%i18n:@total-users%'.replace('{}', total) }}</span>
<span>・</span>
- <a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? '%i18n:!@vote%' : '%i18n:!@show-result%' }}</a>
+ <a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? '%i18n:@vote%' : '%i18n:@show-result%' }}</a>
<span v-if="isVoted">%i18n:@voted%</span>
</p>
</div>
diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue
index e2c8a6ed3f..0db6f66b37 100644
--- a/src/client/app/common/views/components/reaction-picker.vue
+++ b/src/client/app/common/views/components/reaction-picker.vue
@@ -22,7 +22,7 @@
import Vue from 'vue';
import * as anime from 'animejs';
-const placeholder = '%i18n:!@choose-reaction%';
+const placeholder = '%i18n:@choose-reaction%';
export default Vue.extend({
props: ['note', 'source', 'compact', 'cb'],
diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue
index 7fb9fc3fd4..6b9d58e0a8 100644
--- a/src/client/app/common/views/components/signin.vue
+++ b/src/client/app/common/views/components/signin.vue
@@ -9,7 +9,7 @@
<label class="token" v-if="user && user.twoFactorEnabled">
<input v-model="token" type="number" placeholder="%i18n:@token%" required/>%fa:lock%
</label>
- <button type="submit" :disabled="signing">{{ signing ? '%i18n:!@signing-in%' : '%i18n:!@signin%' }}</button>
+ <button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</button>
もしくは <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a>
</form>
</template>
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index 516979acd0..f8bf7dd798 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -127,7 +127,7 @@ export default Vue.extend({
location.href = '/';
});
}).catch(() => {
- alert('%i18n:!@some-error%');
+ alert('%i18n:@some-error%');
(window as any).grecaptcha.reset();
this.recaptchaed = false;
diff --git a/src/client/app/common/views/components/time.vue b/src/client/app/common/views/components/time.vue
index 533958697c..6e0d2b0dcb 100644
--- a/src/client/app/common/views/components/time.vue
+++ b/src/client/app/common/views/components/time.vue
@@ -44,16 +44,16 @@ export default Vue.extend({
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%');
+ 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() {
diff --git a/src/client/app/common/views/components/twitter-setting.vue b/src/client/app/common/views/components/twitter-setting.vue
index ab07e6d09a..9a2a1c3d40 100644
--- a/src/client/app/common/views/components/twitter-setting.vue
+++ b/src/client/app/common/views/components/twitter-setting.vue
@@ -3,7 +3,7 @@
<p>%i18n:@description%<a :href="`${docsUrl}/link-to-twitter`" target="_blank">%i18n:@detail%</a></p>
<p class="account" v-if="os.i.twitter" :title="`Twitter ID: ${os.i.twitter.userId}`">%i18n:@connected-to%: <a :href="`https://twitter.com/${os.i.twitter.screenName}`" target="_blank">@{{ os.i.twitter.screenName }}</a></p>
<p>
- <a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.twitter ? '%i18n:!@reconnect%' : '%i18n:!@connect%' }}</a>
+ <a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.twitter ? '%i18n:@reconnect%' : '%i18n:@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:@disconnect%</a>
</p>
diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
index 50f0877ae9..592367cd6d 100644
--- a/src/client/app/common/views/components/visibility-chooser.vue
+++ b/src/client/app/common/views/components/visibility-chooser.vue
@@ -5,34 +5,34 @@
<div @click="choose('public')" :class="{ active: v == 'public' }">
<div>%fa:globe%</div>
<div>
- <span>公開</span>
+ <span>%i18n:@public%</span>
</div>
</div>
<div @click="choose('home')" :class="{ active: v == 'home' }">
<div>%fa:home%</div>
<div>
- <span>ホーム</span>
- <span>ホームタイムラインにのみ公開</span>
+ <span>%i18n:@home%</span>
+ <span>%i18n:@home-desc%</span>
</div>
</div>
<div @click="choose('followers')" :class="{ active: v == 'followers' }">
<div>%fa:unlock%</div>
<div>
- <span>フォロワー</span>
- <span>自分のフォロワーにのみ公開</span>
+ <span>%i18n:@followers%</span>
+ <span>%i18n:@followers-desc%</span>
</div>
</div>
<div @click="choose('specified')" :class="{ active: v == 'specified' }">
<div>%fa:envelope%</div>
<div>
- <span>ダイレクト</span>
- <span>指定したユーザーにのみ公開</span>
+ <span>%i18n:@specified%</span>
+ <span>%i18n:@specified-desc%</span>
</div>
</div>
<div @click="choose('private')" :class="{ active: v == 'private' }">
<div>%fa:lock%</div>
<div>
- <span>非公開</span>
+ <span>%i18n:@private%</span>
</div>
</div>
</div>
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 6fadb030c3..cad59d24f0 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -37,6 +37,7 @@ export default Vue.extend({
fetch(cb?) {
this.fetching = true;
(this as any).api('notes', {
+ local: true,
reply: false,
renote: false,
media: false,
@@ -52,15 +53,15 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-welcome-timeline
- background #fff
+root(isDark)
+ background isDark ? #282C37 : #fff
> div
padding 16px
overflow-wrap break-word
font-size .9em
- color #4C4C4C
- border-bottom 1px solid rgba(#000, 0.05)
+ color isDark ? #fff : #4C4C4C
+ border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05)
&:after
content ""
@@ -95,17 +96,23 @@ export default Vue.extend({
overflow hidden
font-weight bold
text-overflow ellipsis
- color #627079
+ color isDark ? #fff : #627079
> .username
margin 0 .5em 0 0
- color #ccc
+ color isDark ? #606984 : #ccc
> .info
margin-left auto
font-size 0.9em
> .created-at
- color #c0c0c0
+ color isDark ? #606984 : #c0c0c0
+
+.mk-welcome-timeline[data-darkmode]
+ root(true)
+
+.mk-welcome-timeline:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue
index 75b1d60524..f337cec853 100644
--- a/src/client/app/common/views/widgets/broadcast.vue
+++ b/src/client/app/common/views/widgets/broadcast.vue
@@ -14,7 +14,7 @@
</svg>
</div>
<p class="fetching" v-if="fetching">%i18n:@fetching%<mk-ellipsis/></p>
- <h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:!@no-broadcasts%' : broadcasts[i].title }}</h1>
+ <h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:@no-broadcasts%' : broadcasts[i].title }}</h1>
<p v-if="!fetching">
<span v-if="broadcasts.length != 0" v-html="broadcasts[i].text"></span>
<template v-if="broadcasts.length == 0">%i18n:@have-a-nice-day%</template>
diff --git a/src/client/app/common/views/widgets/donation.vue b/src/client/app/common/views/widgets/donation.vue
index e35462611d..75f5db808a 100644
--- a/src/client/app/common/views/widgets/donation.vue
+++ b/src/client/app/common/views/widgets/donation.vue
@@ -3,9 +3,9 @@
<article>
<h1>%fa:heart%%i18n:@title%</h1>
<p>
- {{ '%i18n:!@text%'.substr(0, '%i18n:!@text%'.indexOf('{')) }}
+ {{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }}
<a href="https://syuilo.com">@syuilo</a>
- {{ '%i18n:!@text%'.substr('%i18n:!@text%'.indexOf('}') + 1) }}
+ {{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }}
</p>
</article>
</div>
diff --git a/src/client/app/config.ts b/src/client/app/config.ts
index 522d7ff056..70c085de1c 100644
--- a/src/client/app/config.ts
+++ b/src/client/app/config.ts
@@ -8,6 +8,7 @@ declare const _STATS_URL_: string;
declare const _STATUS_URL_: string;
declare const _DEV_URL_: string;
declare const _LANG_: string;
+declare const _LANGS_: string;
declare const _RECAPTCHA_SITEKEY_: string;
declare const _SW_PUBLICKEY_: string;
declare const _THEME_COLOR_: string;
@@ -27,6 +28,7 @@ export const statsUrl = _STATS_URL_;
export const statusUrl = _STATUS_URL_;
export const devUrl = _DEV_URL_;
export const lang = _LANG_;
+export const langs = _LANGS_;
export const recaptchaSitekey = _RECAPTCHA_SITEKEY_;
export const swPublickey = _SW_PUBLICKEY_;
export const themeColor = _THEME_COLOR_;
diff --git a/src/client/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue
index 757eefac7e..9a93841e52 100644
--- a/src/client/app/desktop/views/components/calendar.vue
+++ b/src/client/app/desktop/views/components/calendar.vue
@@ -2,7 +2,7 @@
<div class="mk-calendar" :data-melt="design == 4 || design == 5">
<template v-if="design == 0 || design == 1">
<button @click="prev" title="%i18n:@prev%">%fa:chevron-circle-left%</button>
- <p class="title">{{ '%i18n:!@title%'.replace('{1}', year).replace('{2}', month) }}</p>
+ <p class="title">{{ '%i18n:@title%'.replace('{1}', year).replace('{2}', month) }}</p>
<button @click="next" title="%i18n:@next%">%fa:chevron-circle-right%</button>
</template>
@@ -21,7 +21,7 @@
:data-is-out-of-range="isOutOfRange(i + 1)"
:data-is-donichi="isDonichi(i + 1)"
@click="go(i + 1)"
- :title="isOutOfRange(i + 1) ? null : '%i18n:!@go%'"
+ :title="isOutOfRange(i + 1) ? null : '%i18n:@go%'"
>
<div>{{ i + 1 }}</div>
</div>
@@ -58,13 +58,13 @@ export default Vue.extend({
month: new Date().getMonth() + 1,
selected: new Date(),
weekdayText: [
- '%i18n:!common.weekday-short.sunday%',
- '%i18n:!common.weekday-short.monday%',
- '%i18n:!common.weekday-short.tuesday%',
- '%i18n:!common.weekday-short.wednesday%',
- '%i18n:!common.weekday-short.thursday%',
- '%i18n:!common.weekday-short.friday%',
- '%i18n:!common.weekday-short.saturday%'
+ '%i18n:common.weekday-short.sunday%',
+ '%i18n:common.weekday-short.monday%',
+ '%i18n:common.weekday-short.tuesday%',
+ '%i18n:common.weekday-short.wednesday%',
+ '%i18n:common.weekday-short.thursday%',
+ '%i18n:common.weekday-short.friday%',
+ '%i18n:common.weekday-short.saturday%'
]
};
},
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
index d8b8420ece..fb553e1ae7 100644
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ b/src/client/app/desktop/views/components/drive.file.vue
@@ -64,46 +64,46 @@ export default Vue.extend({
this.isContextmenuShowing = true;
contextmenu(e, [{
type: 'item',
- text: '%i18n:!@contextmenu.rename%',
+ text: '%i18n:@contextmenu.rename%',
icon: '%fa:i-cursor%',
onClick: this.rename
}, {
type: 'item',
- text: '%i18n:!@contextmenu.copy-url%',
+ text: '%i18n:@contextmenu.copy-url%',
icon: '%fa:link%',
onClick: this.copyUrl
}, {
type: 'link',
href: `${this.file.url}?download`,
- text: '%i18n:!@contextmenu.download%',
+ text: '%i18n:@contextmenu.download%',
icon: '%fa:download%',
}, {
type: 'divider',
}, {
type: 'item',
- text: '%i18n:!common.delete%',
+ text: '%i18n:common.delete%',
icon: '%fa:R trash-alt%',
onClick: this.deleteFile
}, {
type: 'divider',
}, {
type: 'nest',
- text: '%i18n:!@contextmenu.else-files%',
+ text: '%i18n:@contextmenu.else-files%',
menu: [{
type: 'item',
- text: '%i18n:!@contextmenu.set-as-avatar%',
+ text: '%i18n:@contextmenu.set-as-avatar%',
onClick: this.setAsAvatar
}, {
type: 'item',
- text: '%i18n:!@contextmenu.set-as-banner%',
+ text: '%i18n:@contextmenu.set-as-banner%',
onClick: this.setAsBanner
}]
}, {
type: 'nest',
- text: '%i18n:!@contextmenu.open-in-app%',
+ text: '%i18n:@contextmenu.open-in-app%',
menu: [{
type: 'item',
- text: '%i18n:!@contextmenu.add-app%...',
+ text: '%i18n:@contextmenu.add-app%...',
onClick: this.addApp
}]
}], {
@@ -141,8 +141,8 @@ export default Vue.extend({
rename() {
(this as any).apis.input({
- title: '%i18n:!@contextmenu.rename-file%',
- placeholder: '%i18n:!@contextmenu.input-new-file-name%',
+ title: '%i18n:@contextmenu.rename-file%',
+ placeholder: '%i18n:@contextmenu.input-new-file-name%',
default: this.file.name,
allowEmpty: false
}).then(name => {
@@ -157,9 +157,9 @@ export default Vue.extend({
copyToClipboard(this.file.url);
(this as any).apis.dialog({
title: '%fa:check%%i18n:@contextmenu.copied%',
- text: '%i18n:!@contextmenu.copied-url-to-clipboard%',
+ text: '%i18n:@contextmenu.copied-url-to-clipboard%',
actions: [{
- text: '%i18n:!common.ok%'
+ text: '%i18n:common.ok%'
}]
});
},
diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue
index 0761ffb1a1..16f474f4e0 100644
--- a/src/client/app/desktop/views/components/drive.folder.vue
+++ b/src/client/app/desktop/views/components/drive.folder.vue
@@ -54,26 +54,26 @@ export default Vue.extend({
this.isContextmenuShowing = true;
contextmenu(e, [{
type: 'item',
- text: '%i18n:!@contextmenu.move-to-this-folder%',
+ text: '%i18n:@contextmenu.move-to-this-folder%',
icon: '%fa:arrow-right%',
onClick: this.go
}, {
type: 'item',
- text: '%i18n:!@contextmenu.show-in-new-window%',
+ text: '%i18n:@contextmenu.show-in-new-window%',
icon: '%fa:R window-restore%',
onClick: this.newWindow
}, {
type: 'divider',
}, {
type: 'item',
- text: '%i18n:!@contextmenu.rename%',
+ text: '%i18n:@contextmenu.rename%',
icon: '%fa:i-cursor%',
onClick: this.rename
}, {
type: 'divider',
}, {
type: 'item',
- text: '%i18n:!common.delete%',
+ text: '%i18n:common.delete%',
icon: '%fa:R trash-alt%',
onClick: this.deleteFolder
}], {
@@ -159,15 +159,15 @@ export default Vue.extend({
switch (err) {
case 'detected-circular-definition':
(this as any).apis.dialog({
- title: '%fa:exclamation-triangle%%i18n:!@unable-to-process%',
- text: '%i18n:!@circular-reference-detected%',
+ title: '%fa:exclamation-triangle%%i18n:@unable-to-process%',
+ text: '%i18n:@circular-reference-detected%',
actions: [{
- text: '%i18n:!common.ok%'
+ text: '%i18n:common.ok%'
}]
});
break;
default:
- alert('%i18n:!@unhandled-error% ' + err);
+ alert('%i18n:@unhandled-error% ' + err);
}
});
}
@@ -199,8 +199,8 @@ export default Vue.extend({
rename() {
(this as any).apis.input({
- title: '%i18n:!@contextmenu.rename-folder%',
- placeholder: '%i18n:!@contextmenu.input-new-folder-name%',
+ title: '%i18n:@contextmenu.rename-folder%',
+ placeholder: '%i18n:@contextmenu.input-new-folder-name%',
default: this.folder.name
}).then(name => {
(this as any).api('drive/folders/update', {
diff --git a/src/client/app/desktop/views/components/drive.nav-folder.vue b/src/client/app/desktop/views/components/drive.nav-folder.vue
index 71b2e419d9..40f620875e 100644
--- a/src/client/app/desktop/views/components/drive.nav-folder.vue
+++ b/src/client/app/desktop/views/components/drive.nav-folder.vue
@@ -8,7 +8,7 @@
@drop.stop="onDrop"
>
<template v-if="folder == null">%fa:cloud%</template>
- <span>{{ folder == null ? '%i18n:!@drive%' : folder.name }}</span>
+ <span>{{ folder == null ? '%i18n:@drive%' : folder.name }}</span>
</div>
</template>
diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue
index 973df1014d..cae40f306c 100644
--- a/src/client/app/desktop/views/components/drive.vue
+++ b/src/client/app/desktop/views/components/drive.vue
@@ -138,17 +138,17 @@ export default Vue.extend({
onContextmenu(e) {
contextmenu(e, [{
type: 'item',
- text: '%i18n:!@contextmenu.create-folder%',
+ text: '%i18n:@contextmenu.create-folder%',
icon: '%fa:R folder%',
onClick: this.createFolder
}, {
type: 'item',
- text: '%i18n:!@contextmenu.upload%',
+ text: '%i18n:@contextmenu.upload%',
icon: '%fa:upload%',
onClick: this.selectLocalFile
}, {
type: 'item',
- text: '%i18n:!@contextmenu.url-upload%',
+ text: '%i18n:@contextmenu.url-upload%',
icon: '%fa:cloud-upload-alt%',
onClick: this.urlUpload
}]);
@@ -306,15 +306,15 @@ export default Vue.extend({
switch (err) {
case 'detected-circular-definition':
(this as any).apis.dialog({
- title: '%fa:exclamation-triangle%%i18n:!@unable-to-process%',
- text: '%i18n:!@circular-reference-detected%',
+ title: '%fa:exclamation-triangle%%i18n:@unable-to-process%',
+ text: '%i18n:@circular-reference-detected%',
actions: [{
- text: '%i18n:!common.ok%'
+ text: '%i18n:common.ok%'
}]
});
break;
default:
- alert('%i18n:!@unhandled-error% ' + err);
+ alert('%i18n:@unhandled-error% ' + err);
}
});
}
@@ -327,8 +327,8 @@ export default Vue.extend({
urlUpload() {
(this as any).apis.input({
- title: '%i18n:!@url-upload%',
- placeholder: '%i18n:!@url-of-file%'
+ title: '%i18n:@url-upload%',
+ placeholder: '%i18n:@url-of-file%'
}).then(url => {
(this as any).api('drive/files/upload_from_url', {
url: url,
@@ -337,9 +337,9 @@ export default Vue.extend({
(this as any).apis.dialog({
title: '%fa:check%%i18n:@url-upload-requested%',
- text: '%i18n:!@may-take-time%',
+ text: '%i18n:@may-take-time%',
actions: [{
- text: '%i18n:!common.ok%'
+ text: '%i18n:common.ok%'
}]
});
});
@@ -347,8 +347,8 @@ export default Vue.extend({
createFolder() {
(this as any).apis.input({
- title: '%i18n:!@create-folder%',
- placeholder: '%i18n:!@folder-name%'
+ title: '%i18n:@create-folder%',
+ placeholder: '%i18n:@folder-name%'
}).then(name => {
(this as any).api('drive/folders/create', {
name: name,
diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue
index f3eec13e0b..7ed31315f1 100644
--- a/src/client/app/desktop/views/components/followers-window.vue
+++ b/src/client/app/desktop/views/components/followers-window.vue
@@ -1,7 +1,7 @@
<template>
<mk-window width="400px" height="550px" @closed="$destroy">
<span slot="header" :class="$style.header">
- <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>%i18n:!@followers%.replace('{}', {{ user | userName }})
+ <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }}
</span>
<mk-followers :user="user"/>
</mk-window>
@@ -11,7 +11,12 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['user']
+ props: ['user'],
+ computed: {
+ name(): string {
+ return Vue.filter('userName')(this.user);
+ }
+ }
});
</script>
diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue
index 153819b12e..b97f21e2a3 100644
--- a/src/client/app/desktop/views/components/following-window.vue
+++ b/src/client/app/desktop/views/components/following-window.vue
@@ -1,7 +1,7 @@
<template>
<mk-window width="400px" height="550px" @closed="$destroy">
<span slot="header" :class="$style.header">
- <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>%i18n:!@following%.replace('{}', {{ user | userName }})
+ <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }}
</span>
<mk-following :user="user"/>
</mk-window>
@@ -11,7 +11,12 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['user']
+ props: ['user'],
+ computed: {
+ name(): string {
+ return Vue.filter('userName')(this.user);
+ }
+ }
});
</script>
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index 87dae5a806..d84c1e404f 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -102,7 +102,7 @@ export default Vue.extend({
computed: {
home(): any[] {
- return this.$store.state.settings.data.home;
+ return this.$store.state.settings.home;
},
left(): any[] {
return this.home.filter(w => w.place == 'left');
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index bda53db918..5b48b7a1ba 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -2,16 +2,16 @@
<div class="mk-note-detail" :title="title">
<button
class="read-more"
- v-if="p.reply && p.reply.replyId && context.length == 0"
- title="会話をもっと読み込む"
- @click="fetchContext"
- :disabled="contextFetching"
+ v-if="p.reply && p.reply.replyId && conversation.length == 0"
+ title="%i18n:@more%"
+ @click="fetchConversation"
+ :disabled="conversationFetching"
>
- <template v-if="!contextFetching">%fa:ellipsis-v%</template>
- <template v-if="contextFetching">%fa:spinner .pulse%</template>
+ <template v-if="!conversationFetching">%fa:ellipsis-v%</template>
+ <template v-if="conversationFetching">%fa:spinner .pulse%</template>
</button>
- <div class="context">
- <x-sub v-for="note in context" :key="note.id" :note="note"/>
+ <div class="conversation">
+ <x-sub v-for="note in conversation" :key="note.id" :note="note"/>
</div>
<div class="reply-to" v-if="p.reply">
<x-sub :note="p.reply"/>
@@ -21,7 +21,10 @@
<mk-avatar class="avatar" :user="note.user"/>
%fa:retweet%
<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
- がRenote
+ <span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span>
+ <a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
+ <span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span>
+ <mk-time :time="note.createdAt"/>
</p>
</div>
<article>
@@ -35,7 +38,7 @@
</header>
<div class="body">
<div class="text">
- <span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
+ <span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
<mk-note-html v-if="p.text" :text="p.text" :i="os.i"/>
</div>
<div class="media" v-if="p.media.length > 0">
@@ -46,7 +49,7 @@
<div class="tags" v-if="p.tags && p.tags.length > 0">
<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
</div>
- <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+ <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="map" v-if="p.geo" ref="map"></div>
<div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote"/>
@@ -54,15 +57,15 @@
</div>
<footer>
<mk-reactions-viewer :note="p"/>
- <button @click="reply" title="返信">
+ <button @click="reply" title="">
<template v-if="p.reply">%fa:reply-all%</template>
<template v-else>%fa:reply%</template>
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
</button>
- <button @click="renote" title="Renote">
+ <button @click="renote" title="%i18n:@renote%">
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
</button>
- <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="リアクション">
+ <button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:@add-reaction%">
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
</button>
<button @click="menu" ref="menuButton">
@@ -104,8 +107,8 @@ export default Vue.extend({
data() {
return {
- context: [],
- contextFetching: false,
+ conversation: [],
+ conversationFetching: false,
replies: []
};
},
@@ -173,15 +176,15 @@ export default Vue.extend({
},
methods: {
- fetchContext() {
- this.contextFetching = true;
+ fetchConversation() {
+ this.conversationFetching = true;
- // Fetch context
- (this as any).api('notes/context', {
+ // Fetch conversation
+ (this as any).api('notes/conversation', {
noteId: this.p.replyId
- }).then(context => {
- this.contextFetching = false;
- this.context = context.reverse();
+ }).then(conversation => {
+ this.conversationFetching = false;
+ this.conversation = conversation.reverse();
});
},
reply() {
@@ -246,7 +249,7 @@ root(isDark)
&:disabled
color isDark ? #21242b : #ccc
- > .context
+ > .conversation
> *
border-bottom 1px solid isDark ? #1c2023 : #eef0f2
diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue
index 503982b1a8..5f0c46b4c6 100644
--- a/src/client/app/desktop/views/components/notes.note.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -4,6 +4,9 @@
<div class="main">
<header>
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
+ <span class="is-admin" v-if="note.user.isAdmin">admin</span>
+ <span class="is-bot" v-if="note.user.isBot">bot</span>
+ <span class="is-cat" v-if="note.user.isCat">cat</span>
<span class="username"><mk-acct :user="note.user"/></span>
<div class="info">
<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
@@ -68,7 +71,6 @@ root(isDark)
align-items baseline
margin-bottom 2px
white-space nowrap
- line-height 21px
> .name
display block
@@ -84,6 +86,20 @@ root(isDark)
&:hover
text-decoration underline
+ > .is-admin
+ > .is-bot
+ > .is-cat
+ margin 0 0.5em 0 0
+ padding 1px 5px
+ font-size 10px
+ color isDark ? #758188 : #aaa
+ border solid 1px isDark ? #57616f : #ddd
+ border-radius 3px
+
+ &.is-admin
+ border-color isDark ? #d42c41 : #f56a7b
+ color isDark ? #d42c41 : #f56a7b
+
> .username
margin 0 .5em 0 0
color isDark ? #606984 : #d1d8da
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index e23d3e5a52..4448820eb9 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -6,9 +6,9 @@
<div class="renote" v-if="isRenote">
<mk-avatar class="avatar" :user="note.user"/>
%fa:retweet%
- <span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
+ <span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span>
<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
- <span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span>
+ <span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span>
<mk-time :time="note.createdAt"/>
</div>
<article>
@@ -16,7 +16,9 @@
<div class="main">
<header>
<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
- <span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
+ <span class="is-admin" v-if="p.user.isAdmin">admin</span>
+ <span class="is-bot" v-if="p.user.isBot">bot</span>
+ <span class="is-cat" v-if="p.user.isCat">cat</span>
<span class="username"><mk-acct :user="p.user"/></span>
<div class="info">
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
@@ -430,7 +432,9 @@ root(isDark)
&:hover
text-decoration underline
+ > .is-admin
> .is-bot
+ > .is-cat
margin 0 .5em 0 0
padding 1px 6px
font-size 12px
@@ -438,6 +442,10 @@ root(isDark)
border solid 1px isDark ? #57616f : #ddd
border-radius 3px
+ &.is-admin
+ border-color isDark ? #d42c41 : #f56a7b
+ color isDark ? #d42c41 : #f56a7b
+
> .username
margin 0 .5em 0 0
overflow hidden
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index c041e5278c..55b0de3fbd 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -145,9 +145,9 @@ export default Vue.extend({
this.notes.unshift(note);
// サウンドを再生する
- if ((this as any).os.isEnableSounds && !silent) {
+ if (this.$store.state.device.enableSounds && !silent) {
const sound = new Audio(`${url}/assets/post.mp3`);
- sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+ sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 7923d1a62d..5564dad623 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -81,7 +81,7 @@
</transition-group>
</div>
<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
- <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:!common.loading%' : '%i18n:!@more%' }}
+ <template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
</button>
<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
<p class="loading" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue
index 1f0fbff760..18bb39f9bc 100644
--- a/src/client/app/desktop/views/components/post-form-window.vue
+++ b/src/client/app/desktop/views/components/post-form-window.vue
@@ -4,8 +4,8 @@
<span :class="$style.icon" v-if="geo">%fa:map-marker-alt%</span>
<span v-if="!reply">%i18n:@note%</span>
<span v-if="reply">%i18n:@reply%</span>
- <span :class="$style.count" v-if="media.length != 0">{{ '%i18n:!@attaches%'.replace('{}', media.length) }}</span>
- <span :class="$style.count" v-if="uploadings.length != 0">{{ '%i18n:!@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
+ <span :class="$style.count" v-if="media.length != 0">{{ '%i18n:@attaches%'.replace('{}', media.length) }}</span>
+ <span :class="$style.count" v-if="uploadings.length != 0">{{ '%i18n:@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
</span>
<mk-note-preview v-if="reply" :class="$style.notePreview" :note="reply"/>
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index 984fc9866c..0696d4e82b 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -37,7 +37,7 @@
<button class="visibility" title="公開範囲" @click="setVisibility" ref="visibilityButton">%fa:lock%</button>
<p class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</p>
<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
- {{ posting ? '%i18n:!@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
+ {{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
</button>
<input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
<div class="dropzone" v-if="draghover"></div>
@@ -86,18 +86,18 @@ export default Vue.extend({
placeholder(): string {
return this.renote
- ? '%i18n:!@quote-placeholder%'
+ ? '%i18n:@quote-placeholder%'
: this.reply
- ? '%i18n:!@reply-placeholder%'
- : '%i18n:!@note-placeholder%';
+ ? '%i18n:@reply-placeholder%'
+ : '%i18n:@note-placeholder%';
},
submitText(): string {
return this.renote
- ? '%i18n:!@renote%'
+ ? '%i18n:@renote%'
: this.reply
- ? '%i18n:!@reply%'
- : '%i18n:!@note%';
+ ? '%i18n:@reply%'
+ : '%i18n:@note%';
},
canPost(): boolean {
@@ -304,16 +304,16 @@ export default Vue.extend({
this.deleteDraft();
this.$emit('posted');
(this as any).apis.notify(this.renote
- ? '%i18n:!@reposted%'
+ ? '%i18n:@reposted%'
: this.reply
- ? '%i18n:!@replied%'
- : '%i18n:!@posted%');
+ ? '%i18n:@replied%'
+ : '%i18n:@posted%');
}).catch(err => {
(this as any).apis.notify(this.renote
- ? '%i18n:!@renote-failed%'
+ ? '%i18n:@renote-failed%'
: this.reply
- ? '%i18n:!@reply-failed%'
- : '%i18n:!@note-failed%');
+ ? '%i18n:@reply-failed%'
+ : '%i18n:@note-failed%');
}).then(() => {
this.posting = false;
});
diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue
index 9c0154211b..38eab3362f 100644
--- a/src/client/app/desktop/views/components/renote-form.vue
+++ b/src/client/app/desktop/views/components/renote-form.vue
@@ -5,7 +5,7 @@
<footer>
<a class="quote" v-if="!quote" @click="onQuote">%i18n:@quote%</a>
<button class="ui cancel" @click="cancel">%i18n:@cancel%</button>
- <button class="ui primary ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:!@reposting%' : '%i18n:!@renote%' }}</button>
+ <button class="ui primary ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:@reposting%' : '%i18n:@renote%' }}</button>
</footer>
</template>
<template v-if="quote">
@@ -32,9 +32,9 @@ export default Vue.extend({
renoteId: this.note.id
}).then(data => {
this.$emit('posted');
- (this as any).apis.notify('%i18n:!@success%');
+ (this as any).apis.notify('%i18n:@success%');
}).catch(err => {
- (this as any).apis.notify('%i18n:!@failure%');
+ (this as any).apis.notify('%i18n:@failure%');
}).then(() => {
this.wait = false;
});
diff --git a/src/client/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue
index d5be177dcc..deb865b102 100644
--- a/src/client/app/desktop/views/components/settings-window.vue
+++ b/src/client/app/desktop/views/components/settings-window.vue
@@ -1,6 +1,6 @@
<template>
<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy">
- <span slot="header" :class="$style.header">%fa:cog%設定</span>
+ <span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span>
<mk-settings @done="close"/>
</mk-window>
</template>
diff --git a/src/client/app/desktop/views/components/settings.2fa.vue b/src/client/app/desktop/views/components/settings.2fa.vue
index 99b6cb947c..0809dd798c 100644
--- a/src/client/app/desktop/views/components/settings.2fa.vue
+++ b/src/client/app/desktop/views/components/settings.2fa.vue
@@ -34,7 +34,7 @@ export default Vue.extend({
methods: {
register() {
(this as any).apis.input({
- title: '%i18n:!@enter-password%',
+ title: '%i18n:@enter-password%',
type: 'password'
}).then(password => {
(this as any).api('i/2fa/register', {
@@ -47,13 +47,13 @@ export default Vue.extend({
unregister() {
(this as any).apis.input({
- title: '%i18n:!@enter-password%',
+ title: '%i18n:@enter-password%',
type: 'password'
}).then(password => {
(this as any).api('i/2fa/unregister', {
password: password
}).then(() => {
- (this as any).apis.notify('%i18n:!@unregistered%');
+ (this as any).apis.notify('%i18n:@unregistered%');
(this as any).os.i.twoFactorEnabled = false;
});
});
@@ -63,10 +63,10 @@ export default Vue.extend({
(this as any).api('i/2fa/done', {
token: this.token
}).then(() => {
- (this as any).apis.notify('%i18n:!@success%');
+ (this as any).apis.notify('%i18n:@success%');
(this as any).os.i.twoFactorEnabled = true;
}).catch(() => {
- (this as any).apis.notify('%i18n:!@failed%');
+ (this as any).apis.notify('%i18n:@failed%');
});
}
}
diff --git a/src/client/app/desktop/views/components/settings.api.vue b/src/client/app/desktop/views/components/settings.api.vue
index b22ee6cdab..b8eef3de63 100644
--- a/src/client/app/desktop/views/components/settings.api.vue
+++ b/src/client/app/desktop/views/components/settings.api.vue
@@ -15,7 +15,7 @@ export default Vue.extend({
methods: {
regenerateToken() {
(this as any).apis.input({
- title: '%i18n:!@enter-password%',
+ title: '%i18n:@enter-password%',
type: 'password'
}).then(password => {
(this as any).api('i/regenerate_token', {
diff --git a/src/client/app/desktop/views/components/settings.password.vue b/src/client/app/desktop/views/components/settings.password.vue
index 9e89bc0f6e..39896daf67 100644
--- a/src/client/app/desktop/views/components/settings.password.vue
+++ b/src/client/app/desktop/views/components/settings.password.vue
@@ -11,21 +11,21 @@ export default Vue.extend({
methods: {
reset() {
(this as any).apis.input({
- title: '%i18n:!@enter-current-password%',
+ title: '%i18n:@enter-current-password%',
type: 'password'
}).then(currentPassword => {
(this as any).apis.input({
- title: '%i18n:!@enter-new-password%',
+ title: '%i18n:@enter-new-password%',
type: 'password'
}).then(newPassword => {
(this as any).apis.input({
- title: '%i18n:!@enter-new-password-again%',
+ title: '%i18n:@enter-new-password-again%',
type: 'password'
}).then(newPassword2 => {
if (newPassword !== newPassword2) {
(this as any).apis.dialog({
title: null,
- text: '%i18n:!@not-match%',
+ text: '%i18n:@not-match%',
actions: [{
text: 'OK'
}]
@@ -36,7 +36,7 @@ export default Vue.extend({
currentPasword: currentPassword,
newPassword: newPassword
}).then(() => {
- (this as any).apis.notify('%i18n:!@changed%');
+ (this as any).apis.notify('%i18n:@changed%');
});
});
});
diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue
index 84b09eb988..132ab12f1c 100644
--- a/src/client/app/desktop/views/components/settings.profile.vue
+++ b/src/client/app/desktop/views/components/settings.profile.vue
@@ -24,7 +24,8 @@
<button class="ui primary" @click="save">%i18n:@save%</button>
<section>
<h2>その他</h2>
- <mk-switch v-model="os.i.isBot" @change="onChangeIsBot" text="このアカウントはbotです"/>
+ <mk-switch v-model="os.i.isBot" @change="onChangeIsBot" text="%i18n:@is-bot%"/>
+ <mk-switch v-model="os.i.isCat" @change="onChangeIsCat" text="%i18n:@is-cat%"/>
</section>
</div>
</template>
@@ -65,6 +66,11 @@ export default Vue.extend({
(this as any).api('i/update', {
isBot: (this as any).os.i.isBot
});
+ },
+ onChangeIsCat() {
+ (this as any).api('i/update', {
+ isCat: (this as any).os.i.isCat
+ });
}
}
});
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 6652a8ac37..dac5fe67cb 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -62,8 +62,10 @@
<el-slider
v-model="soundVolume"
:show-input="true"
- :format-tooltip="v => `${v}%`"
+ :format-tooltip="v => `${v * 100}%`"
:disabled="!enableSounds"
+ :max="1"
+ :step="0.1"
/>
<button class="ui button" @click="soundTest">%fa:volume-up% %i18n:@test%</button>
</section>
@@ -77,14 +79,10 @@
<h1>%i18n:@language%</h1>
<el-select v-model="lang" placeholder="%i18n:@pick-language%">
<el-option-group label="%i18n:@recommended%">
- <el-option label="%i18n:@auto%" value=""/>
+ <el-option label="%i18n:@auto%" :value="null"/>
</el-option-group>
<el-option-group label="%i18n:@specify-language%">
- <el-option label="日本語" value="ja"/>
- <el-option label="English" value="en"/>
- <el-option label="Français" value="fr"/>
- <el-option label="Polski" value="pl"/>
- <el-option label="Deutsch" value="de"/>
+ <el-option v-for="x in langs" :label="x[1]" :value="x[0]" :key="x[0]"/>
</el-option-group>
</el-select>
<div class="none ui info">
@@ -178,15 +176,7 @@
<mk-switch v-model="debug" text="%i18n:@debug-mode%">
<span>%i18n:@debug-mode-desc%</span>
</mk-switch>
- <template v-if="debug">
- <mk-switch v-model="useRawScript" text="%i18n:@use-raw-script%">
- <span>%i18n:@use-raw-script-desc%</span>
- </mk-switch>
- <div class="none ui info">
- <p>%fa:info-circle%%i18n:@source-info%</p>
- </div>
- </template>
- <mk-switch v-model="enableExperimental" text="%i18n:@experimental%">
+ <mk-switch v-model="enableExperimentalFeatures" text="%i18n:@experimental%">
<span>%i18n:@experimental-desc%</span>
</mk-switch>
<details v-if="debug">
@@ -214,7 +204,7 @@ import XApi from './settings.api.vue';
import XApps from './settings.apps.vue';
import XSignins from './settings.signins.vue';
import XDrive from './settings.drive.vue';
-import { url, docsUrl, license, lang, version } from '../../../config';
+import { url, docsUrl, license, lang, langs, version } from '../../../config';
import checkForUpdate from '../../../common/scripts/check-for-update';
import MkTaskManager from './taskmanager.vue';
@@ -235,55 +225,59 @@ export default Vue.extend({
meta: null,
license,
version,
+ langs,
latestVersion: undefined,
- checkingForUpdate: false,
- darkmode: localStorage.getItem('darkmode') == 'true',
- enableSounds: localStorage.getItem('enableSounds') == 'true',
- autoPopout: localStorage.getItem('autoPopout') == 'true',
- apiViaStream: localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true,
- soundVolume: localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) : 50,
- lang: localStorage.getItem('lang') || '',
- preventUpdate: localStorage.getItem('preventUpdate') == 'true',
- debug: localStorage.getItem('debug') == 'true',
- useRawScript: localStorage.getItem('useRawScript') == 'true',
- enableExperimental: localStorage.getItem('enableExperimental') == 'true'
+ checkingForUpdate: false
};
},
computed: {
licenseUrl(): string {
return `${docsUrl}/${lang}/license`;
- }
- },
- watch: {
- autoPopout() {
- localStorage.setItem('autoPopout', this.autoPopout ? 'true' : 'false');
},
- apiViaStream() {
- localStorage.setItem('apiViaStream', this.apiViaStream ? 'true' : 'false');
+
+ apiViaStream: {
+ get() { return this.$store.state.device.apiViaStream; },
+ set(value) { this.$store.commit('device/set', { key: 'apiViaStream', value }); }
},
- darkmode() {
- (this as any)._updateDarkmode_(this.darkmode);
+
+ autoPopout: {
+ get() { return this.$store.state.device.autoPopout; },
+ set(value) { this.$store.commit('device/set', { key: 'autoPopout', value }); }
},
- enableSounds() {
- localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false');
+
+ darkmode: {
+ get() { return this.$store.state.device.darkmode; },
+ set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
},
- soundVolume() {
- localStorage.setItem('soundVolume', this.soundVolume.toString());
+
+ enableSounds: {
+ get() { return this.$store.state.device.enableSounds; },
+ set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }
},
- lang() {
- localStorage.setItem('lang', this.lang);
+
+ soundVolume: {
+ get() { return this.$store.state.device.soundVolume; },
+ set(value) { this.$store.commit('device/set', { key: 'soundVolume', value }); }
},
- preventUpdate() {
- localStorage.setItem('preventUpdate', this.preventUpdate ? 'true' : 'false');
+
+ lang: {
+ get() { return this.$store.state.device.lang; },
+ set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
},
- debug() {
- localStorage.setItem('debug', this.debug ? 'true' : 'false');
+
+ preventUpdate: {
+ get() { return this.$store.state.device.preventUpdate; },
+ set(value) { this.$store.commit('device/set', { key: 'preventUpdate', value }); }
},
- useRawScript() {
- localStorage.setItem('useRawScript', this.useRawScript ? 'true' : 'false');
+
+ debug: {
+ get() { return this.$store.state.device.debug; },
+ set(value) { this.$store.commit('device/set', { key: 'debug', value }); }
},
- enableExperimental() {
- localStorage.setItem('enableExperimental', this.enableExperimental ? 'true' : 'false');
+
+ enableExperimentalFeatures: {
+ get() { return this.$store.state.device.enableExperimentalFeatures; },
+ set(value) { this.$store.commit('device/set', { key: 'enableExperimentalFeatures', value }); }
}
},
created() {
@@ -371,13 +365,13 @@ export default Vue.extend({
this.latestVersion = newer;
if (newer == null) {
(this as any).apis.dialog({
- title: '%i18n:!@no-updates%',
- text: '%i18n:!@no-updates-desc%'
+ title: '%i18n:@no-updates%',
+ text: '%i18n:@no-updates-desc%'
});
} else {
(this as any).apis.dialog({
- title: '%i18n:!@update-available%',
- text: '%i18n:!@update-available-desc%'
+ title: '%i18n:@update-available%',
+ text: '%i18n:@update-available-desc%'
});
}
});
@@ -385,13 +379,13 @@ export default Vue.extend({
clean() {
localStorage.clear();
(this as any).apis.dialog({
- title: '%i18n:!@cache-cleared%',
- text: '%i18n:!@caache-cleared-desc%'
+ title: '%i18n:@cache-cleared%',
+ text: '%i18n:@caache-cleared-desc%'
});
},
soundTest() {
const sound = new Audio(`${url}/assets/message.mp3`);
- sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+ sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
}
diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue
index dd4012039b..03b634b0ff 100644
--- a/src/client/app/desktop/views/components/sub-note-content.vue
+++ b/src/client/app/desktop/views/components/sub-note-content.vue
@@ -1,17 +1,17 @@
<template>
<div class="mk-sub-note-content">
<div class="body">
- <span v-if="note.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
+ <span v-if="note.isHidden" style="opacity: 0.5">%i18n:@hidden%</span>
<a class="reply" v-if="note.replyId">%fa:reply%</a>
<mk-note-html :text="note.text" :i="os.i"/>
<a class="rp" v-if="note.renoteId" :href="`/note:${note.renoteId}`">RP: ...</a>
</div>
<details v-if="note.media.length > 0">
- <summary>({{ note.media.length }}つのメディア)</summary>
+ <summary>({{ note.media.length }}%i18n:@media%)</summary>
<mk-media-list :media-list="note.media"/>
</details>
<details v-if="note.poll">
- <summary>投票</summary>
+ <summary>%i18n:@poll%</summary>
<mk-poll :note="note"/>
</details>
</div>
diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue
index fd15ea6006..f3f6539496 100644
--- a/src/client/app/desktop/views/components/ui.header.account.vue
+++ b/src/client/app/desktop/views/components/ui.header.account.vue
@@ -35,7 +35,7 @@
</ul>
<ul>
<li @click="dark">
- <p><span>%i18n:@dark%</span><template v-if="_darkmode_">%fa:moon%</template><template v-else>%fa:R moon%</template></p>
+ <p><span>%i18n:@dark%</span><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></p>
</li>
</ul>
</div>
@@ -99,7 +99,10 @@ export default Vue.extend({
(this as any).os.signout();
},
dark() {
- (this as any)._updateDarkmode_(!(this as any)._darkmode_);
+ this.$store.commit('device/set', {
+ key: 'darkmode',
+ value: !this.$store.state.device.darkmode
+ });
}
}
});
diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue
index 585c0a864f..454c725d20 100644
--- a/src/client/app/desktop/views/components/user-lists-window.vue
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -2,7 +2,7 @@
<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
<span slot="header">%fa:list% リスト</span>
- <div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="_darkmode_">
+ <div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="$store.state.device.darkmode">
<button class="ui" @click="add">%i18n:@create-list%</button>
<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a>
</div>
diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue
index dbad295178..262fd38cd1 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -7,7 +7,7 @@
<span class="username">@{{ user | acct }}</span>
</header>
<div class="body">
- <p class="followed" v-if="user.isFollowed">フォローされています</p>
+ <p class="followed" v-if="user.isFollowed">%i18n:@followed%</p>
<div class="description">{{ user.description }}</div>
</div>
</div>
diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
index 2e7eb557b4..ac06ac8e57 100644
--- a/src/client/app/desktop/views/components/window.vue
+++ b/src/client/app/desktop/views/components/window.vue
@@ -9,8 +9,8 @@
>
<h1><slot name="header"></slot></h1>
<div>
- <button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="ポップアウト">%fa:R window-restore%</button>
- <button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="閉じる">%fa:times%</button>
+ <button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="%i18n:@popout%">%fa:R window-restore%</button>
+ <button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="%i18n:@close%">%fa:times%</button>
</div>
</header>
<div class="content">
@@ -95,7 +95,7 @@ export default Vue.extend({
},
created() {
- if (localStorage.getItem('autoPopout') == 'true' && this.popoutUrl) {
+ if ((this as any).os.store.state.device.autoPopout && this.popoutUrl) {
this.popout();
this.preventMount = true;
} else {
diff --git a/src/client/app/desktop/views/pages/drive.vue b/src/client/app/desktop/views/pages/drive.vue
index 353f59b703..217dcb7751 100644
--- a/src/client/app/desktop/views/pages/drive.vue
+++ b/src/client/app/desktop/views/pages/drive.vue
@@ -16,11 +16,11 @@ export default Vue.extend({
this.folder = this.$route.params.folder;
},
mounted() {
- document.title = 'Misskey Drive';
+ document.title = '%i18n:@title%';
},
methods: {
onMoveRoot() {
- const title = 'Misskey Drive';
+ const title = '%i18n:@title%';
// Rewrite URL
history.pushState(null, title, '/i/drive');
@@ -28,7 +28,7 @@ export default Vue.extend({
document.title = title;
},
onOpenFolder(folder) {
- const title = folder.name + ' | Misskey Drive';
+ const title = folder.name + ' | %i18n:@title%';
// Rewrite URL
history.pushState(null, title, '/i/drive/folder/' + folder.id);
@@ -49,4 +49,3 @@ export default Vue.extend({
> .mk-drive
height 100%
</style>
-
diff --git a/src/client/app/desktop/views/pages/favorites.vue b/src/client/app/desktop/views/pages/favorites.vue
index d908c08f7c..71d36cdf2b 100644
--- a/src/client/app/desktop/views/pages/favorites.vue
+++ b/src/client/app/desktop/views/pages/favorites.vue
@@ -4,7 +4,7 @@
<template v-for="favorite in favorites">
<mk-note-detail :note="favorite.note" :key="favorite.note.id"/>
</template>
- <a v-if="existMore" @click="more">さらに読み込む</a>
+ <a v-if="existMore" @click="more">%i18n:@more%</a>
</main>
</mk-ui>
</template>
diff --git a/src/client/app/desktop/views/pages/home-customize.vue b/src/client/app/desktop/views/pages/home-customize.vue
index 8aa06be57f..da5f15bb69 100644
--- a/src/client/app/desktop/views/pages/home-customize.vue
+++ b/src/client/app/desktop/views/pages/home-customize.vue
@@ -6,7 +6,7 @@
import Vue from 'vue';
export default Vue.extend({
mounted() {
- document.title = 'Misskey - ホームのカスタマイズ';
+ document.title = 'Misskey - %i18n:@title%';
}
});
</script>
diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue
index 1cc8d8a778..06c32776c9 100644
--- a/src/client/app/desktop/views/pages/messaging-room.vue
+++ b/src/client/app/desktop/views/pages/messaging-room.vue
@@ -21,10 +21,21 @@ export default Vue.extend({
$route: 'fetch'
},
created() {
+ const applyBg = v =>
+ document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important');
+
+ applyBg(this.$store.state.device.darkmode);
+
+ this.unwatchDarkmode = this.$store.watch(s => {
+ return s.device.darkmode;
+ }, applyBg);
+
this.fetch();
},
- mounted() {
- document.documentElement.style.background = '#fff';
+ beforeDestroy() {
+ document.documentElement.style.removeProperty('background');
+ document.documentElement.style.removeProperty('background-color'); // for safari's bug
+ this.unwatchDarkmode();
},
methods: {
fetch() {
@@ -50,6 +61,5 @@ export default Vue.extend({
flex 1
flex-direction column
min-height 100%
- background #fff
</style>
diff --git a/src/client/app/desktop/views/pages/selectdrive.vue b/src/client/app/desktop/views/pages/selectdrive.vue
index 7a00896640..c846f2418f 100644
--- a/src/client/app/desktop/views/pages/selectdrive.vue
+++ b/src/client/app/desktop/views/pages/selectdrive.vue
@@ -29,7 +29,7 @@ export default Vue.extend({
}
},
mounted() {
- document.title = '%i18n:!@title%';
+ document.title = '%i18n:@title%';
},
methods: {
onSelected(file) {
diff --git a/src/client/app/desktop/views/pages/user-list.users.vue b/src/client/app/desktop/views/pages/user-list.users.vue
index 4236cdbb14..517fe89750 100644
--- a/src/client/app/desktop/views/pages/user-list.users.vue
+++ b/src/client/app/desktop/views/pages/user-list.users.vue
@@ -1,8 +1,8 @@
<template>
<div>
<mk-widget-container>
- <template slot="header">%fa:users% ユーザー</template>
- <button slot="func" title="ユーザーを追加" @click="add">%fa:plus%</button>
+ <template slot="header">%fa:users% %i18n:@users%</template>
+ <button slot="func" title="%i18n:@add-user%" @click="add">%fa:plus%</button>
<div data-id="d0b63759-a822-4556-a5ce-373ab966e08a">
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw% %i18n:common.loading%<mk-ellipsis/></p>
@@ -48,7 +48,7 @@ export default Vue.extend({
methods: {
add() {
(this as any).apis.input({
- title: 'ユーザー名',
+ title: '%i18n:@username%',
}).then(async username => {
const user = await (this as any).api('users/show', {
username
diff --git a/src/client/app/desktop/views/pages/user/user.timeline.vue b/src/client/app/desktop/views/pages/user/user.timeline.vue
index 9c9840c190..576a285104 100644
--- a/src/client/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/client/app/desktop/views/pages/user/user.timeline.vue
@@ -1,15 +1,15 @@
<template>
<div class="timeline">
<header>
- <span :data-active="mode == 'default'" @click="mode = 'default'">投稿</span>
- <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿と返信</span>
- <span :data-active="mode == 'with-media'" @click="mode = 'with-media'">メディア</span>
+ <span :data-active="mode == 'default'" @click="mode = 'default'">%i18n:@default%</span>
+ <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">%i18n:@with-replies%</span>
+ <span :data-active="mode == 'with-media'" @click="mode = 'with-media'">%i18n:@with-media%</span>
</header>
<div class="loading" v-if="fetching">
<mk-ellipsis-icon/>
</div>
<mk-notes ref="timeline" :more="existMore ? more : null">
- <p class="empty" slot="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p>
+ <p class="empty" slot="empty">%fa:R comments%%i18n:@empty%</p>
</mk-notes>
</div>
</template>
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 898b6b2179..91ad4b61c3 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -1,23 +1,16 @@
<template>
<div class="mk-welcome">
+ <button @click="dark">
+ <template v-if="$store.state.device.darkmode">%fa:moon%</template>
+ <template v-else>%fa:R moon%</template>
+ </button>
<main>
- <div class="top">
- <div>
- <div>
- <h1>Share<br><span ref="share">Everything!</span><span class="cursor">_</span></h1>
- <p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p>
- <p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
- <div class="users">
- <mk-avatar class="avatar" v-for="user in users" :key="user.id" :user="user"/>
- </div>
- </div>
- <div>
- <div>
- <header>%fa:comments R% タイムライン<div><span></span><span></span><span></span></div></header>
- <mk-welcome-timeline/>
- </div>
- </div>
- </div>
+ <img :src="$store.state.device.darkmode ? 'assets/title-dark.svg' : 'assets/title.svg'" alt="Misskey">
+ <p><button class="signup" @click="signup">%i18n:@signup-button%</button><button class="signin" @click="signin">%i18n:@signin-button%</button></p>
+
+ <div class="tl">
+ <header>%fa:comments R% %i18n:@timeline%<div><span></span><span></span><span></span></div></header>
+ <mk-welcome-timeline/>
</div>
</main>
<mk-forkit/>
@@ -28,11 +21,11 @@
</div>
</footer>
<modal name="signup" width="500px" height="auto" scrollable>
- <header :class="$style.signupFormHeader">新規登録</header>
+ <header :class="$style.signupFormHeader">%i18n:@signup%</header>
<mk-signup :class="$style.signupForm"/>
</modal>
<modal name="signin" width="500px" height="auto" scrollable>
- <header :class="$style.signinFormHeader">ログイン</header>
+ <header :class="$style.signinFormHeader">%i18n:@signin%</header>
<mk-signin :class="$style.signinForm"/>
</modal>
</div>
@@ -42,64 +35,25 @@
import Vue from 'vue';
import { docsUrl, copyright, lang } from '../../../config';
-const shares = [
- 'Everything!',
- 'Webpages',
- 'Photos',
- 'Interests',
- 'Favorites'
-];
-
export default Vue.extend({
data() {
return {
aboutUrl: `${docsUrl}/${lang}/about`,
- copyright,
- users: [],
- clock: null,
- i: 0
+ copyright
};
},
- mounted() {
- (this as any).api('users', {
- sort: '+follower',
- limit: 20
- }).then(users => {
- this.users = users;
- });
-
- this.clock = setInterval(() => {
- if (++this.i == shares.length) this.i = 0;
- const speed = 70;
- const text = (this.$refs.share as any).innerText;
- for (let i = 0; i < text.length; i++) {
- setTimeout(() => {
- if (this.$refs.share) {
- (this.$refs.share as any).innerText = text.substr(0, text.length - i);
- }
- }, i * speed)
- }
- setTimeout(() => {
- const newText = shares[this.i];
- for (let i = 0; i <= newText.length; i++) {
- setTimeout(() => {
- if (this.$refs.share) {
- (this.$refs.share as any).innerText = newText.substr(0, i);
- }
- }, i * speed)
- }
- }, text.length * speed);
- }, 4000);
- },
- beforeDestroy() {
- clearInterval(this.clock);
- },
methods: {
signup() {
this.$modal.show('signup');
},
signin() {
this.$modal.show('signin');
+ },
+ dark() {
+ this.$store.commit('device/set', {
+ key: 'darkmode',
+ value: !this.$store.state.device.darkmode
+ });
}
}
});
@@ -115,161 +69,107 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-@import url('https://fonts.googleapis.com/css?family=Sarpanch:700')
-
-.mk-welcome
+root(isDark)
display flex
flex-direction column
flex 1
- $width = 1000px
-
- background linear-gradient(to bottom, #1e1d65, #bd6659)
- //background-image url('/assets/welcome-bg.svg')
- background-size cover
- background-position top center
- &:before
- content ""
- display block
- position fixed
- bottom 0
+ > button
+ position absolute
+ z-index 1
+ top 0
left 0
- width 100%
- height 100%
- background-image url('/assets/welcome-fg.svg')
- background-size cover
- background-position bottom center
+ padding 16px
+ font-size 18px
+ color isDark ? #fff : #555
> main
- display flex
flex 1
+ padding 64px 0 0 0
+ text-align center
+ color isDark ? #9aa4b3 : #555
- > .top
- display flex
- width 100%
-
- > div
- display flex
- max-width $width + 64px
- margin 0 auto
- padding 80px 32px 0 32px
-
- > *
- margin-bottom 48px
+ > img
+ width 350px
- > div:first-child
- margin-right 48px
- color #fff
- text-shadow 0 0 12px #172062
+ > p
+ margin 8px 0
+ line-height 2em
- > h1
- margin 0
- font-weight bold
- //font-variant small-caps
- letter-spacing 12px
- font-family 'Sarpanch', sans-serif
- font-size 42px
- line-height 48px
+ button
+ padding 8px 16px
+ font-size inherit
- > .cursor
- animation cursor 1s infinite linear both
+ .signup
+ color $theme-color
+ border solid 2px $theme-color
+ border-radius 4px
- @keyframes cursor
- 0%
- opacity 1
- 50%
- opacity 0
+ &:focus
+ box-shadow 0 0 0 3px rgba($theme-color, 0.2)
- > p
- margin 1em 0
- line-height 2em
+ &:hover
+ color $theme-color-foreground
+ background $theme-color
- button
- padding 8px 16px
- font-size inherit
+ &:active
+ color $theme-color-foreground
+ background darken($theme-color, 10%)
+ border-color darken($theme-color, 10%)
- .signup
- color $theme-color
- border solid 2px $theme-color
- border-radius 4px
+ .signin
+ &:hover
+ color isDark ? #fff : #000
- &:focus
- box-shadow 0 0 0 3px rgba($theme-color, 0.2)
+ > .tl
+ margin 32px auto 0 auto
+ width 410px
+ text-align left
+ background isDark ? #313543 : #fff
+ border-radius 8px
+ box-shadow 0 8px 32px rgba(#000, 0.15)
+ overflow hidden
- &:hover
- color $theme-color-foreground
- background $theme-color
+ > header
+ z-index 1
+ padding 12px 16px
+ color isDark ? #e3e5e8 : #888d94
+ box-shadow 0 1px 0px rgba(#000, 0.1)
- &:active
- color $theme-color-foreground
- background darken($theme-color, 10%)
- border-color darken($theme-color, 10%)
+ > div
+ position absolute
+ top 0
+ right 0
+ padding inherit
- .signin
- &:hover
- color #fff
+ > span
+ display inline-block
+ height 11px
+ width 11px
+ margin-left 6px
+ border-radius 100%
+ vertical-align middle
- > .users
- margin 16px 0 0 0
+ &:nth-child(1)
+ background #5BCC8B
- > *
- display inline-block
- margin 4px
- width 38px
- height 38px
- border-radius 6px
+ &:nth-child(2)
+ background #E6BB46
- > div:last-child
+ &:nth-child(3)
+ background #DF7065
- > div
- width 410px
- background #fff
- border-radius 8px
- box-shadow 0 0 0 12px rgba(#000, 0.1)
- overflow hidden
-
- > header
- z-index 1
- padding 12px 16px
- color #888d94
- box-shadow 0 1px 0px rgba(#000, 0.1)
-
- > div
- position absolute
- top 0
- right 0
- padding inherit
-
- > span
- display inline-block
- height 11px
- width 11px
- margin-left 6px
- background #ccc
- border-radius 100%
- vertical-align middle
-
- &:nth-child(1)
- background #5BCC8B
-
- &:nth-child(2)
- background #E6BB46
-
- &:nth-child(3)
- background #DF7065
-
- > .mk-welcome-timeline
- max-height 350px
- overflow auto
+ > .mk-welcome-timeline
+ max-height 350px
+ overflow auto
> footer
font-size 12px
- color #949ea5
+ color isDark ? #949ea5 : #737c82
> div
- max-width $width
margin 0 auto
- padding 0 0 42px 0
+ padding 64px
text-align center
> .c
@@ -277,6 +177,12 @@ export default Vue.extend({
font-size 10px
opacity 0.7
+.mk-welcome[data-darkmode]
+ root(true)
+
+.mk-welcome:not([data-darkmode])
+ root(false)
+
</style>
<style lang="stylus" module>
diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
index 36fcc20636..7421a81102 100644
--- a/src/client/app/desktop/views/widgets/polls.vue
+++ b/src/client/app/desktop/views/widgets/polls.vue
@@ -4,7 +4,7 @@
<template slot="header">%fa:chart-pie%%i18n:@title%</template>
<button slot="func" title="%i18n:@refresh%" @click="fetch">%fa:sync%</button>
- <div class="mkw-polls--body" :data-darkmode="_darkmode_">
+ <div class="mkw-polls--body" :data-darkmode="$store.state.device.darkmode">
<div class="poll" v-if="!fetching && poll != null">
<p v-if="poll.text"><router-link to="poll | notePage">{{ poll.text }}</router-link></p>
<p v-if="!poll.text"><router-link to="poll | notePage">%fa:link%</router-link></p>
diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
index 3b01ed034d..5af5b88e23 100644
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ b/src/client/app/desktop/views/widgets/profile.vue
@@ -5,12 +5,12 @@
>
<div class="banner"
:style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=256)` : ''"
- title="クリックでバナー編集"
+ title="%i18n:@update-banner%"
@click="os.apis.updateBanner"
></div>
<mk-avatar class="avatar" :user="os.i"
@click="os.apis.updateAvatar"
- title="クリックでアバター編集"
+ title="%i18n:@update-avatar%"
/>
<router-link class="name" :to="os.i | userPage">{{ os.i | userName }}</router-link>
<p class="username">@{{ os.i | acct }}</p>
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
index 4908b73b23..560ab1a096 100644
--- a/src/client/app/init.ts
+++ b/src/client/app/init.ts
@@ -49,48 +49,6 @@ Vue.mixin({
}
});
-// Dark/Light
-const bus = new Vue();
-Vue.mixin({
- data() {
- return {
- _darkmode_: localStorage.getItem('darkmode') == 'true'
- };
- },
- beforeCreate() {
- // なぜか警告が出るので
- this._darkmode_ = localStorage.getItem('darkmode') == 'true';
- },
- beforeDestroy() {
- bus.$off('updated', this._onDarkmodeUpdated_);
- },
- mounted() {
- this._onDarkmodeUpdated_(this._darkmode_);
- bus.$on('updated', this._onDarkmodeUpdated_);
- },
- methods: {
- _updateDarkmode_(v) {
- localStorage.setItem('darkmode', v.toString());
- if (v) {
- document.documentElement.setAttribute('data-darkmode', 'true');
- } else {
- document.documentElement.removeAttribute('data-darkmode');
- }
- bus.$emit('updated', v);
- },
- _onDarkmodeUpdated_(v) {
- if (!this.$el || !this.$el.setAttribute) return;
- if (v) {
- this.$el.setAttribute('data-darkmode', 'true');
- } else {
- this.$el.removeAttribute('data-darkmode');
- }
- this._darkmode_ = v;
- this.$forceUpdate();
- }
- }
-});
-
/**
* APP ENTRY POINT!
*/
@@ -113,7 +71,7 @@ html.setAttribute('lang', lang);
const head = document.getElementsByTagName('head')[0];
const meta = document.createElement('meta');
meta.setAttribute('name', 'description');
-meta.setAttribute('content', '%i18n:!common.misskey%');
+meta.setAttribute('content', '%i18n:common.misskey%');
head.appendChild(meta);
//#endregion
@@ -141,13 +99,52 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
const launch = (router: VueRouter, api?: (os: MiOS) => API) => {
os.apis = api ? api(os) : null;
+ //#region Dark/Light
+ Vue.mixin({
+ data() {
+ return {
+ _unwatchDarkmode_: null
+ };
+ },
+ mounted() {
+ const apply = v => {
+ if (this.$el.setAttribute == null) return;
+ if (v) {
+ this.$el.setAttribute('data-darkmode', 'true');
+ } else {
+ this.$el.removeAttribute('data-darkmode');
+ }
+ };
+
+ apply(os.store.state.device.darkmode);
+
+ this._unwatchDarkmode_ = os.store.watch(s => {
+ return s.device.darkmode;
+ }, apply);
+ },
+ beforeDestroy() {
+ this._unwatchDarkmode_();
+ }
+ });
+
+ os.store.watch(s => {
+ return s.device.darkmode;
+ }, v => {
+ if (v) {
+ document.documentElement.setAttribute('data-darkmode', 'true');
+ } else {
+ document.documentElement.removeAttribute('data-darkmode');
+ }
+ });
+ //#endregion
+
Vue.mixin({
data() {
return {
os,
api: os.api,
apis: os.apis,
- clientSettings: os.store.state.settings.data
+ clientSettings: os.store.state.settings
};
}
});
@@ -173,7 +170,7 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
}
//#region 更新チェック
- const preventUpdate = localStorage.getItem('preventUpdate') == 'true';
+ const preventUpdate = os.store.state.device.preventUpdate;
if (!preventUpdate) {
setTimeout(() => {
checkForUpdate(os);
diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts
index 2373b0d8d2..a5a38a5414 100644
--- a/src/client/app/mios.ts
+++ b/src/client/app/mios.ts
@@ -98,14 +98,7 @@ export default class MiOS extends EventEmitter {
* Whether is debug mode
*/
public get debug() {
- return localStorage.getItem('debug') == 'true';
- }
-
- /**
- * Whether enable sounds
- */
- public get isEnableSounds() {
- return localStorage.getItem('enableSounds') == 'true';
+ return this.store ? this.store.state.device.debug : false;
}
public store: ReturnType<typeof initStore>;
@@ -435,12 +428,8 @@ export default class MiOS extends EventEmitter {
});
});
- // Whether use raw version script
- const raw = (localStorage.getItem('useRawScript') == 'true' && this.debug)
- || process.env.NODE_ENV != 'production';
-
// The path of service worker script
- const sw = `/sw.${version}.${lang}.${raw ? 'raw' : 'min'}.js`;
+ const sw = `/sw.${version}.${lang}.js`;
// Register service worker
navigator.serviceWorker.register(sw).then(registration => {
@@ -471,8 +460,7 @@ export default class MiOS extends EventEmitter {
};
const promise = new Promise((resolve, reject) => {
- const viaStream = this.stream && this.stream.hasConnection &&
- (localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true);
+ const viaStream = this.stream && this.stream.hasConnection && this.store.state.device.apiViaStream;
if (viaStream) {
const stream = this.stream.borrow();
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index 427c177a14..5418aac090 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -5,7 +5,7 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
-import { MdCard, MdButton, MdField, MdMenu, MdList, MdSwitch } from 'vue-material/dist/components';
+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';
@@ -37,7 +37,6 @@ import MkSearch from './views/pages/search.vue';
import MkFollowers from './views/pages/followers.vue';
import MkFollowing from './views/pages/following.vue';
import MkSettings from './views/pages/settings.vue';
-import MkProfileSetting from './views/pages/profile-setting.vue';
import MkOthello from './views/pages/othello.vue';
Vue.use(MdCard);
@@ -46,6 +45,10 @@ Vue.use(MdField);
Vue.use(MdMenu);
Vue.use(MdList);
Vue.use(MdSwitch);
+Vue.use(MdSubheader);
+Vue.use(MdDialog);
+Vue.use(MdDialogAlert);
+Vue.use(MdRadio);
/**
* init
@@ -67,8 +70,7 @@ init((launch) => {
routes: [
{ path: '/', name: 'index', component: MkIndex },
{ path: '/signup', name: 'signup', component: MkSignup },
- { path: '/i/settings', component: MkSettings },
- { path: '/i/settings/profile', component: MkProfileSetting },
+ { path: '/i/settings', name: 'settings', component: MkSettings },
{ path: '/i/notifications', name: 'notifications', component: MkNotifications },
{ path: '/i/widgets', name: 'widgets', component: MkWidgets },
{ path: '/i/messaging', name: 'messaging', component: MkMessaging },
diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue
index ef3432a3ec..8e35e6c88b 100644
--- a/src/client/app/mobile/views/components/drive.vue
+++ b/src/client/app/mobile/views/components/drive.vue
@@ -32,7 +32,7 @@
<div class="files" v-if="files.length > 0">
<x-file v-for="file in files" :key="file.id" :file="file"/>
<button class="more" v-if="moreFiles" @click="fetchMoreFiles">
- {{ fetchingMoreFiles ? '%i18n:!common.loading%' : '%i18n:!@load-more%' }}
+ {{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:@load-more%' }}
</button>
</div>
<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
diff --git a/src/client/app/mobile/views/components/follow-button.vue b/src/client/app/mobile/views/components/follow-button.vue
index 5d6b8ebf84..a6b5cf0556 100644
--- a/src/client/app/mobile/views/components/follow-button.vue
+++ b/src/client/app/mobile/views/components/follow-button.vue
@@ -7,7 +7,7 @@
<template v-if="!wait && user.isFollowing">%fa:minus%</template>
<template v-if="!wait && !user.isFollowing">%fa:plus%</template>
<template v-if="wait">%fa:spinner .pulse .fw%</template>
- {{ user.isFollowing ? '%i18n:!@unfollow%' : '%i18n:!@follow%' }}
+ {{ user.isFollowing ? '%i18n:@unfollow%' : '%i18n:@follow%' }}
</button>
</template>
diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue
index 9e0f8e5f7e..c2f9c66e84 100644
--- a/src/client/app/mobile/views/components/media-image.vue
+++ b/src/client/app/mobile/views/components/media-image.vue
@@ -17,9 +17,17 @@ export default Vue.extend({
},
computed: {
style(): any {
+ let url = `url(${this.image.url}?thumbnail)`;
+
+ if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) {
+ url = null;
+ } else if (this.raw || this.$store.state.device.loadRawImages) {
+ url = `url(${this.image.url})`;
+ }
+
return {
'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
- 'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
+ 'background-image': url
};
}
}
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index c6664a91da..244dbb6c03 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -2,15 +2,15 @@
<div class="mk-note-detail">
<button
class="more"
- v-if="p.reply && p.reply.replyId && context.length == 0"
- @click="fetchContext"
- :disabled="fetchingContext"
+ v-if="p.reply && p.reply.replyId && conversation.length == 0"
+ @click="fetchConversation"
+ :disabled="conversationFetching"
>
- <template v-if="!contextFetching">%fa:ellipsis-v%</template>
- <template v-if="contextFetching">%fa:spinner .pulse%</template>
+ <template v-if="!conversationFetching">%fa:ellipsis-v%</template>
+ <template v-if="conversationFetching">%fa:spinner .pulse%</template>
</button>
- <div class="context">
- <x-sub v-for="note in context" :key="note.id" :note="note"/>
+ <div class="conversation">
+ <x-sub v-for="note in conversation" :key="note.id" :note="note"/>
</div>
<div class="reply-to" v-if="p.reply">
<x-sub :note="p.reply"/>
@@ -99,8 +99,8 @@ export default Vue.extend({
data() {
return {
- context: [],
- contextFetching: false,
+ conversation: [],
+ conversationFetching: false,
replies: []
};
},
@@ -166,14 +166,14 @@ export default Vue.extend({
methods: {
fetchContext() {
- this.contextFetching = true;
+ this.conversationFetching = true;
- // Fetch context
- (this as any).api('notes/context', {
+ // Fetch conversation
+ (this as any).api('notes/conversation', {
noteId: this.p.replyId
- }).then(context => {
- this.contextFetching = false;
- this.context = context.reverse();
+ }).then(conversation => {
+ this.conversationFetching = false;
+ this.conversation = conversation.reverse();
});
},
reply() {
@@ -245,7 +245,7 @@ root(isDark)
&:disabled
color #ccc
- > .context
+ > .conversation
> *
border-bottom 1px solid isDark ? #1c2023 : #eef0f2
diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
index b55cad792d..8fa57768e0 100644
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -1,9 +1,13 @@
<template>
-<div class="mk-note-preview">
- <mk-avatar class="avatar" :user="note.user"/>
+<div class="mk-note-preview" :class="{ smart: $store.state.device.postStyle == 'smart' }">
+ <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
<header>
+ <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+ <span class="is-admin" v-if="note.user.isAdmin">admin</span>
+ <span class="is-bot" v-if="note.user.isBot">bot</span>
+ <span class="is-cat" v-if="note.user.isCat">cat</span>
<span class="username"><mk-acct :user="note.user"/></span>
<router-link class="time" :to="note | notePage">
<mk-time :time="note.createdAt"/>
@@ -35,6 +39,13 @@ root(isDark)
display block
clear both
+ &.smart
+ > .main
+ width 100%
+
+ > header
+ align-items center
+
> .avatar
display block
float left
@@ -53,6 +64,13 @@ root(isDark)
margin-bottom 4px
white-space nowrap
+ > .avatar
+ flex-shrink 0
+ margin-right 8px
+ width 18px
+ height 18px
+ border-radius 100%
+
> .name
display block
margin 0 .5em 0 0
@@ -65,8 +83,19 @@ root(isDark)
text-decoration none
text-overflow ellipsis
- &:hover
- text-decoration underline
+ > .is-admin
+ > .is-bot
+ > .is-cat
+ margin 0 0.5em 0 0
+ padding 1px 6px
+ font-size 10px
+ color isDark ? #758188 : #aaa
+ border solid 1px isDark ? #57616f : #ddd
+ border-radius 3px
+
+ &.is-admin
+ border-color isDark ? #d42c41 : #f56a7b
+ color isDark ? #d42c41 : #f56a7b
> .username
margin 0 .5em 0 0
diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue
index 2fb3b2ffcc..149a78ecde 100644
--- a/src/client/app/mobile/views/components/note.sub.vue
+++ b/src/client/app/mobile/views/components/note.sub.vue
@@ -1,9 +1,13 @@
<template>
-<div class="sub">
- <mk-avatar class="avatar" :user="note.user"/>
+<div class="sub" :class="{ smart: $store.state.device.postStyle == 'smart' }">
+ <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
<header>
+ <mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+ <span class="is-admin" v-if="note.user.isAdmin">admin</span>
+ <span class="is-bot" v-if="note.user.isBot">bot</span>
+ <span class="is-cat" v-if="note.user.isCat">cat</span>
<span class="username"><mk-acct :user="note.user"/></span>
<div class="info">
<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
@@ -42,6 +46,13 @@ root(isDark)
@media (min-width 600px)
padding 24px 32px
+ &.smart
+ > .main
+ width 100%
+
+ > header
+ align-items center
+
&:after
content ""
display block
@@ -73,6 +84,13 @@ root(isDark)
margin-bottom 2px
white-space nowrap
+ > .avatar
+ flex-shrink 0
+ margin-right 8px
+ width 18px
+ height 18px
+ border-radius 100%
+
> .name
display block
margin 0 0.5em 0 0
@@ -88,6 +106,20 @@ root(isDark)
&:hover
text-decoration underline
+ > .is-admin
+ > .is-bot
+ > .is-cat
+ margin 0 0.5em 0 0
+ padding 1px 5px
+ font-size 10px
+ color isDark ? #758188 : #aaa
+ border solid 1px isDark ? #57616f : #ddd
+ border-radius 3px
+
+ &.is-admin
+ border-color isDark ? #d42c41 : #f56a7b
+ color isDark ? #d42c41 : #f56a7b
+
> .username
text-align left
margin 0
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index 83a957cfbd..2004263d22 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -1,22 +1,25 @@
<template>
-<div class="note" :class="{ renote: isRenote }">
+<div class="note" :class="{ renote: isRenote, smart: $store.state.device.postStyle == 'smart' }">
<div class="reply-to" v-if="p.reply && (!os.isSignedIn || clientSettings.showReplyTarget)">
<x-sub :note="p.reply"/>
</div>
<div class="renote" v-if="isRenote">
<mk-avatar class="avatar" :user="note.user"/>
%fa:retweet%
- <span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
+ <span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span>
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
- <span>{{ '%i18n:!@reposted-by%'.substr('%i18n:!@reposted-by%'.indexOf('}') + 1) }}</span>
+ <span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span>
<mk-time :time="note.createdAt"/>
</div>
<article>
- <mk-avatar class="avatar" :user="p.user"/>
+ <mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
<header>
+ <mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle == 'smart'"/>
<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
- <span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
+ <span class="is-admin" v-if="p.user.isAdmin">admin</span>
+ <span class="is-bot" v-if="p.user.isBot">bot</span>
+ <span class="is-cat" v-if="p.user.isCat">cat</span>
<span class="username"><mk-acct :user="p.user"/></span>
<div class="info">
<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
@@ -262,6 +265,15 @@ root(isDark)
@media (min-width 500px)
font-size 16px
+ &.smart
+ > article
+ > .main
+ width 100%
+
+ > header
+ align-items center
+ margin-bottom 4px
+
> .renote
display flex
align-items center
@@ -278,12 +290,17 @@ root(isDark)
padding 16px 32px
.avatar
+ flex-shrink 0
display inline-block
- width 28px
- height 28px
+ width 20px
+ height 20px
margin 0 8px 0 0
border-radius 6px
+ @media (min-width 500px)
+ width 28px
+ height 28px
+
[data-fa]
margin-right 4px
@@ -352,21 +369,26 @@ root(isDark)
@media (min-width 500px)
margin-bottom 2px
+ > .avatar
+ flex-shrink 0
+ margin-right 8px
+ width 20px
+ height 20px
+ border-radius 100%
+
> .name
display block
margin 0 0.5em 0 0
padding 0
overflow hidden
color isDark ? #fff : #627079
- font-size 1em
font-weight bold
text-decoration none
text-overflow ellipsis
- &:hover
- text-decoration underline
-
+ > .is-admin
> .is-bot
+ > .is-cat
margin 0 0.5em 0 0
padding 1px 6px
font-size 12px
@@ -374,6 +396,10 @@ root(isDark)
border solid 1px isDark ? #57616f : #ddd
border-radius 3px
+ &.is-admin
+ border-color isDark ? #d42c41 : #f56a7b
+ color isDark ? #d42c41 : #f56a7b
+
> .username
margin 0 0.5em 0 0
overflow hidden
diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue
index 8ab66940c4..6bb9e9bb2c 100644
--- a/src/client/app/mobile/views/components/notifications.vue
+++ b/src/client/app/mobile/views/components/notifications.vue
@@ -12,7 +12,7 @@
<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>
- {{ fetchingMoreNotifications ? '%i18n:!common.loading%' : '%i18n:!@more%' }}
+ {{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
</button>
<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index 0bb498e5d7..b3b5ffd502 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -20,7 +20,7 @@
<a @click="addVisibleUser">+ユーザーを追加</a>
</div>
<input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)">
- <textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:!@reply-placeholder%' : renote ? '%i18n:!@renote-placeholder%' : '%i18n:!@note-placeholder%'"></textarea>
+ <textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:@reply-placeholder%' : renote ? '%i18n:@renote-placeholder%' : '%i18n:@note-placeholder%'"></textarea>
<div class="attaches" v-show="files.length != 0">
<x-draggable class="files" :list="files" :options="{ animation: 150 }">
<div class="file" v-for="file in files" :key="file.id">
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index ec42dbc99d..aa469bd1c8 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -28,8 +28,8 @@
<li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li>
</ul>
<ul>
- <li><router-link to="/i/settings">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li>
- <li @click="dark"><p><template v-if="_darkmode_">%fa:moon%</template><template v-else>%fa:R moon%</template><span>ダークモード</span></p></li>
+ <li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li>
+ <li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>ダークモード</span></p></li>
</ul>
</div>
<a :href="aboutUrl"><p class="about">%i18n:@about%</p></a>
@@ -94,7 +94,7 @@ export default Vue.extend({
},
methods: {
search() {
- const query = window.prompt('%i18n:!@search%');
+ const query = window.prompt('%i18n:@search%');
if (query == null || query == '') return;
this.$router.push('/search?q=' + encodeURIComponent(query));
},
@@ -117,7 +117,10 @@ export default Vue.extend({
this.hasGameInvitations = false;
},
dark() {
- (this as any)._updateDarkmode_(!(this as any)._darkmode_);
+ this.$store.commit('device/set', {
+ key: 'darkmode',
+ value: !this.$store.state.device.darkmode
+ });
}
}
});
diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue
index 52c82115bf..808ee72402 100644
--- a/src/client/app/mobile/views/components/user-card.vue
+++ b/src/client/app/mobile/views/components/user-card.vue
@@ -1,9 +1,7 @@
<template>
<div class="mk-user-card">
<header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''">
- <a :href="user | userPage">
- <img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/>
- </a>
+ <mk-avatar class="avatar" :user="user"/>
</header>
<a class="name" :href="user | userPage" target="_blank">{{ user | userName }}</a>
<p class="username"><mk-acct :user="user"/></p>
@@ -35,15 +33,14 @@ export default Vue.extend({
background-position center
border-radius 8px 8px 0 0
- > a
- > img
- position absolute
- top 20px
- left calc(50% - 40px)
- width 80px
- height 80px
- border solid 2px #fff
- border-radius 8px
+ > .avatar
+ position absolute
+ top 20px
+ left calc(50% - 40px)
+ width 80px
+ height 80px
+ border solid 2px #fff
+ border-radius 8px
> .name
display block
diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue
index 3ceb876596..aca6f783b8 100644
--- a/src/client/app/mobile/views/components/user-timeline.vue
+++ b/src/client/app/mobile/views/components/user-timeline.vue
@@ -3,7 +3,7 @@
<mk-notes ref="timeline" :more="existMore ? more : null">
<div slot="empty">
%fa:R comments%
- {{ withMedia ? '%i18n:!@no-notes-with-media%' : '%i18n:!@no-notes%' }}
+ {{ withMedia ? '%i18n:@no-notes-with-media%' : '%i18n:@no-notes%' }}
</div>
</mk-notes>
</div>
diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue
index 33ade94e35..dfb9c62142 100644
--- a/src/client/app/mobile/views/pages/followers.vue
+++ b/src/client/app/mobile/views/pages/followers.vue
@@ -2,7 +2,7 @@
<mk-ui>
<template slot="header" v-if="!fetching">
<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
- {{ '%i18n:!@followers-of%'.replace('{}', name) }}
+ {{ '%i18n:@followers-of%'.replace('{}', name) }}
</template>
<mk-users-list
v-if="!fetching"
@@ -49,7 +49,7 @@ export default Vue.extend({
this.user = user;
this.fetching = false;
- document.title = '%i18n:!@followers-of%'.replace('{}', this.name) + ' | Misskey';
+ document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | Misskey';
});
},
onLoaded() {
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
index c6d6d44281..35461ea2fc 100644
--- a/src/client/app/mobile/views/pages/following.vue
+++ b/src/client/app/mobile/views/pages/following.vue
@@ -2,7 +2,7 @@
<mk-ui>
<template slot="header" v-if="!fetching">
<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
- {{ '%i18n:!@following-of%'.replace('{}', name) }}
+ {{ '%i18n:@following-of%'.replace('{}', name) }}
</template>
<mk-users-list
v-if="!fetching"
@@ -48,7 +48,7 @@ export default Vue.extend({
this.user = user;
this.fetching = false;
- document.title = '%i18n:!@followers-of%'.replace('{}', this.name) + ' | Misskey';
+ document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | Misskey';
});
},
onLoaded() {
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index ad6d5ed408..5701ff03d5 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -2,9 +2,9 @@
<mk-ui>
<span slot="header" @click="showNav = true">
<span>
- <span v-if="src == 'home'">%fa:home%ホーム</span>
- <span v-if="src == 'local'">%fa:R comments%ローカル</span>
- <span v-if="src == 'global'">%fa:globe%グローバル</span>
+ <span v-if="src == 'home'">%fa:home%%i18n:@home%</span>
+ <span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span>
+ <span v-if="src == 'global'">%fa:globe%%i18n:@global%</span>
<span v-if="src.startsWith('list')">%fa:list%{{ list.title }}</span>
</span>
<span style="margin-left:8px">
@@ -17,14 +17,14 @@
<button @click="fn">%fa:pencil-alt%</button>
</template>
- <main :data-darkmode="_darkmode_">
+ <main :data-darkmode="$store.state.device.darkmode">
<div class="nav" v-if="showNav">
<div class="bg" @click="showNav = false"></div>
<div class="body">
<div>
- <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span>
- <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% ローカル</span>
- <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% グローバル</span>
+ <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span>
+ <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span>
+ <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
<template v-if="lists">
<span v-for="l in lists" :data-active="src == 'list:' + l.id" @click="src = 'list:' + l.id; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
</template>
diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue
index c26a9b735e..8b82b03fb9 100644
--- a/src/client/app/mobile/views/pages/messaging-room.vue
+++ b/src/client/app/mobile/views/pages/messaging-room.vue
@@ -16,16 +16,30 @@ export default Vue.extend({
data() {
return {
fetching: true,
- user: null
+ user: null,
+ unwatchDarkmode: null
};
},
watch: {
$route: 'fetch'
},
created() {
- document.documentElement.style.background = '#fff';
+ const applyBg = v =>
+ document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important');
+
+ applyBg(this.$store.state.device.darkmode);
+
+ this.unwatchDarkmode = this.$store.watch(s => {
+ return s.device.darkmode;
+ }, applyBg);
+
this.fetch();
},
+ beforeDestroy() {
+ document.documentElement.style.removeProperty('background');
+ document.documentElement.style.removeProperty('background-color'); // for safari's bug
+ this.unwatchDarkmode();
+ },
methods: {
fetch() {
this.fetching = true;
@@ -39,4 +53,3 @@ export default Vue.extend({
}
});
</script>
-
diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue
index cc328e5a1c..057470efd9 100644
--- a/src/client/app/mobile/views/pages/messaging.vue
+++ b/src/client/app/mobile/views/pages/messaging.vue
@@ -12,7 +12,6 @@ import getAcct from '../../../../../acct/render';
export default Vue.extend({
mounted() {
document.title = 'Misskey %i18n:@messaging%';
- document.documentElement.style.background = '#fff';
},
methods: {
navigate(user) {
diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue
index d0c0fe9535..2e98201caa 100644
--- a/src/client/app/mobile/views/pages/notifications.vue
+++ b/src/client/app/mobile/views/pages/notifications.vue
@@ -21,7 +21,7 @@ export default Vue.extend({
},
methods: {
fn() {
- const ok = window.confirm('%i18n:!@read-all%');
+ const ok = window.confirm('%i18n:@read-all%');
if (!ok) return;
(this as any).api('notifications/markAsRead_all');
diff --git a/src/client/app/mobile/views/pages/profile-setting.vue b/src/client/app/mobile/views/pages/profile-setting.vue
deleted file mode 100644
index 7048cdef31..0000000000
--- a/src/client/app/mobile/views/pages/profile-setting.vue
+++ /dev/null
@@ -1,225 +0,0 @@
-<template>
-<mk-ui>
- <span slot="header">%fa:user%%i18n:@title%</span>
- <div :class="$style.content">
- <p>%fa:info-circle%%i18n:@will-be-published%</p>
- <div :class="$style.form">
- <div :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=1024)` : ''" @click="setBanner">
- <img :src="`${os.i.avatarUrl}?thumbnail&size=200`" alt="avatar" @click="setAvatar"/>
- </div>
- <label>
- <p>%i18n:@name%</p>
- <input v-model="name" type="text"/>
- </label>
- <label>
- <p>%i18n:@location%</p>
- <input v-model="location" type="text"/>
- </label>
- <label>
- <p>%i18n:@description%</p>
- <textarea v-model="description"></textarea>
- </label>
- <label>
- <p>%i18n:@birthday%</p>
- <input v-model="birthday" type="date"/>
- </label>
- <label>
- <p>%i18n:@avatar%</p>
- <button @click="setAvatar" :disabled="avatarSaving">%i18n:@set-avatar%</button>
- </label>
- <label>
- <p>%i18n:@banner%</p>
- <button @click="setBanner" :disabled="bannerSaving">%i18n:@set-banner%</button>
- </label>
- </div>
- <button :class="$style.save" @click="save" :disabled="saving">%fa:check%%i18n:@save%</button>
- </div>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
- data() {
- return {
- name: null,
- location: null,
- description: null,
- birthday: null,
- avatarSaving: false,
- bannerSaving: false,
- saving: false
- };
- },
- created() {
- this.name = (this as any).os.i.name || '';
- this.location = (this as any).os.i.profile.location;
- this.description = (this as any).os.i.description;
- this.birthday = (this as any).os.i.profile.birthday;
- },
- mounted() {
- document.title = 'Misskey | %i18n:@title%';
- },
- methods: {
- setAvatar() {
- (this as any).apis.chooseDriveFile({
- multiple: false
- }).then(file => {
- this.avatarSaving = true;
-
- (this as any).api('i/update', {
- avatarId: file.id
- }).then(() => {
- this.avatarSaving = false;
- alert('%i18n:!@avatar-saved%');
- });
- });
- },
- setBanner() {
- (this as any).apis.chooseDriveFile({
- multiple: false
- }).then(file => {
- this.bannerSaving = true;
-
- (this as any).api('i/update', {
- bannerId: file.id
- }).then(() => {
- this.bannerSaving = false;
- alert('%i18n:!@banner-saved%');
- });
- });
- },
- save() {
- this.saving = true;
-
- (this as any).api('i/update', {
- name: this.name || null,
- location: this.location || null,
- description: this.description || null,
- birthday: this.birthday || null
- }).then(() => {
- this.saving = false;
- alert('%i18n:!@saved%');
- });
- }
- }
-});
-</script>
-
-<style lang="stylus" module>
-@import '~const.styl'
-
-.content
- margin 8px auto
- max-width 500px
- width calc(100% - 16px)
-
- @media (min-width 500px)
- margin 16px auto
- width calc(100% - 32px)
-
- > p
- display block
- margin 0 0 8px 0
- padding 12px 16px
- font-size 14px
- color #79d4e6
- border solid 1px #71afbb
- //color #276f86
- //background #f8ffff
- //border solid 1px #a9d5de
- border-radius 8px
-
- > [data-fa]
- margin-right 6px
-
-.form
- position relative
- background #fff
- box-shadow 0 0 0 1px rgba(#000, 0.2)
- border-radius 8px
-
- &:before
- content ""
- display block
- position absolute
- bottom -20px
- left calc(50% - 10px)
- border-top solid 10px rgba(#000, 0.2)
- border-right solid 10px transparent
- border-bottom solid 10px transparent
- border-left solid 10px transparent
-
- &:after
- content ""
- display block
- position absolute
- bottom -16px
- left calc(50% - 8px)
- border-top solid 8px #fff
- border-right solid 8px transparent
- border-bottom solid 8px transparent
- border-left solid 8px transparent
-
- > div
- height 128px
- background-color #e4e4e4
- background-size cover
- background-position center
- border-radius 8px 8px 0 0
-
- > img
- position absolute
- top 25px
- left calc(50% - 40px)
- width 80px
- height 80px
- border solid 2px #fff
- border-radius 8px
-
- > label
- display block
- margin 0
- padding 16px
- border-bottom solid 1px #eee
-
- &:last-of-type
- border none
-
- > p:first-child
- display block
- margin 0
- padding 0 0 4px 0
- font-weight bold
- color #2f3c42
-
- > input[type="text"]
- > textarea
- display block
- width 100%
- padding 12px
- font-size 16px
- color #192427
- border solid 2px #ddd
- border-radius 4px
-
- > textarea
- min-height 80px
-
-.save
- display block
- margin 8px 0 0 0
- padding 16px
- width 100%
- font-size 16px
- color $theme-color-foreground
- background $theme-color
- border-radius 8px
-
- &:disabled
- opacity 0.7
-
- > [data-fa]
- margin-right 4px
-
-</style>
diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue
index f038a6f81f..9850fbcfb4 100644
--- a/src/client/app/mobile/views/pages/search.vue
+++ b/src/client/app/mobile/views/pages/search.vue
@@ -3,7 +3,7 @@
<span slot="header">%fa:search% {{ q }}</span>
<main v-if="!fetching">
<mk-notes :class="$style.notes" :notes="notes">
- <span v-if="notes.length == 0">{{ '%i18n:!@empty%'.replace('{}', q) }}</span>
+ <span v-if="notes.length == 0">{{ '%i18n:@empty%'.replace('{}', q) }}</span>
<button v-if="existMore" @click="more" :disabled="fetching" slot="tail">
<span v-if="!fetching">%i18n:@load-more%</span>
<span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span>
diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue
index d730e4fcff..1a162b346c 100644
--- a/src/client/app/mobile/views/pages/selectdrive.vue
+++ b/src/client/app/mobile/views/pages/selectdrive.vue
@@ -25,7 +25,7 @@ export default Vue.extend({
}
},
mounted() {
- document.title = '%i18n:!@title%';
+ document.title = '%i18n:@title%';
},
methods: {
onSelected(file) {
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index b16860d62c..3bb25f88f8 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -2,13 +2,13 @@
<mk-ui>
<span slot="header">%fa:cog%%i18n:@settings%</span>
<main>
- <p v-html="'%i18n:!@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p>
+ <p v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p>
<div>
<x-profile/>
- <md-card class="md-layout-item md-size-50 md-small-size-100">
+ <md-card>
<md-card-header>
- <div class="md-title">%i18n:@design%</div>
+ <div class="md-title">%fa:palette% %i18n:@design%</div>
</md-card-header>
<md-card-content>
@@ -19,6 +19,110 @@
<div>
<md-switch v-model="clientSettings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</md-switch>
</div>
+
+ <div>
+ <div class="md-body-2">%i18n:@timeline%</div>
+
+ <div>
+ <md-switch v-model="clientSettings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</md-switch>
+ </div>
+
+ <div>
+ <md-switch v-model="clientSettings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</md-switch>
+ </div>
+
+ <div>
+ <md-switch v-model="clientSettings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</md-switch>
+ </div>
+ </div>
+
+ <div>
+ <div class="md-body-2">%i18n:@post-style%</div>
+
+ <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>
+
+ <md-card>
+ <md-card-header>
+ <div class="md-title">%fa:cog% %i18n:@behavior%</div>
+ </md-card-header>
+
+ <md-card-content>
+ <div>
+ <md-switch v-model="clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</md-switch>
+ </div>
+
+ <div>
+ <md-switch v-model="clientSettings.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="clientSettings.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="os.i.twitter"><a :href="`https://twitter.com/${os.i.twitter.screenName}`" target="_blank">@{{ os.i.twitter.screenName }}</a></p>
+ <p>
+ <a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ os.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
+ <span v-if="os.i.twitter"> or </span>
+ <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.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>
@@ -29,7 +133,8 @@
<script lang="ts">
import Vue from 'vue';
-import { version, codename } from '../../../config';
+import { apiUrl, version, codename, langs } from '../../../config';
+import checkForUpdate from '../../../common/scripts/check-for-update';
import XProfile from './settings/settings.profile.vue';
@@ -40,22 +145,44 @@ export default Vue.extend({
data() {
return {
+ apiUrl,
version,
codename,
- darkmode: localStorage.getItem('darkmode') == 'true'
+ langs,
+ latestVersion: undefined,
+ checkingForUpdate: false
};
},
computed: {
name(): string {
return Vue.filter('userName')((this as any).os.i);
- }
- },
+ },
- watch: {
- darkmode() {
- (this as any)._updateDarkmode_(this.darkmode);
- }
+ darkmode: {
+ get() { return this.$store.state.device.darkmode; },
+ set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
+ },
+
+ postStyle: {
+ get() { return this.$store.state.device.postStyle; },
+ set(value) { this.$store.commit('device/set', { key: 'postStyle', value }); }
+ },
+
+ lightmode: {
+ get() { return this.$store.state.device.lightmode; },
+ set(value) { this.$store.commit('device/set', { key: 'lightmode', value }); }
+ },
+
+ loadRawImages: {
+ get() { return this.$store.state.device.loadRawImages; },
+ set(value) { this.$store.commit('device/set', { key: 'loadRawImages', value }); }
+ },
+
+ lang: {
+ get() { return this.$store.state.device.lang; },
+ set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
+ },
},
mounted() {
@@ -67,19 +194,83 @@ export default Vue.extend({
(this as any).os.signout();
},
+ onChangeFetchOnScroll(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'fetchOnScroll',
+ value: v
+ });
+ },
+
+ onChangeDisableViaMobile(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'disableViaMobile',
+ value: v
+ });
+ },
+
+ onChangeLoadRemoteMedia(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'loadRemoteMedia',
+ value: v
+ });
+ },
+
onChangeCircleIcons(v) {
this.$store.dispatch('settings/set', {
key: 'circleIcons',
value: v
});
+ },
+
+ onChangeShowReplyTarget(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'showReplyTarget',
+ value: v
+ });
+ },
+
+ onChangeShowMyRenotes(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'showMyRenotes',
+ value: v
+ });
+ },
+
+ onChangeShowRenotedMyNotes(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'showRenotedMyNotes',
+ value: v
+ });
+ },
+
+ checkForUpdate() {
+ this.checkingForUpdate = true;
+ checkForUpdate((this as any).os, true, true).then(newer => {
+ this.checkingForUpdate = false;
+ this.latestVersion = newer;
+ if (newer == null) {
+ (this as any).apis.dialog({
+ title: '%i18n:@no-updates%',
+ text: '%i18n:@no-updates-desc%'
+ });
+ } else {
+ (this as any).apis.dialog({
+ title: '%i18n:@update-available%',
+ text: '%i18n:@update-available-desc%'
+ });
+ }
+ });
}
}
});
</script>
<style lang="stylus" scoped>
-main
+root(isDark)
padding 0 16px
+ margin 0 auto
+ max-width 500px
+ width 100%
> div
> *
@@ -89,57 +280,12 @@ main
display block
margin 24px
text-align center
- color #cad2da
-
- > ul
- $radius = 8px
-
- display block
- margin 16px auto
- padding 0
- max-width 500px
- width calc(100% - 32px)
- list-style none
- background #fff
- border solid 1px rgba(#000, 0.2)
- border-radius $radius
-
- > li
- display block
- border-bottom solid 1px #ddd
-
- &:hover
- background rgba(#000, 0.1)
-
- &:first-child
- border-top-left-radius $radius
- border-top-right-radius $radius
-
- &:last-child
- border-bottom-left-radius $radius
- border-bottom-right-radius $radius
- border-bottom none
-
- > a
- $height = 48px
-
- display block
- position relative
- padding 0 16px
- line-height $height
- color #4d635e
+ color isDark ? #cad2da : #a2a9b1
- > [data-fa]:nth-of-type(1)
- margin-right 4px
+main[data-darkmode]
+ root(true)
- > [data-fa]:nth-of-type(2)
- display block
- position absolute
- top 0
- right 8px
- z-index 1
- padding 0 20px
- font-size 1.2em
- line-height $height
+main:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/mobile/views/pages/settings/settings.profile.vue b/src/client/app/mobile/views/pages/settings/settings.profile.vue
index 6b5d07cce9..c16c44e133 100644
--- a/src/client/app/mobile/views/pages/settings/settings.profile.vue
+++ b/src/client/app/mobile/views/pages/settings/settings.profile.vue
@@ -1,50 +1,55 @@
<template>
- <md-card class="md-layout-item md-size-50 md-small-size-100">
+ <md-card>
<md-card-header>
- <div class="md-title">%i18n:@title%</div>
+ <div class="md-title">%fa:pencil-alt% %i18n:@title%</div>
</md-card-header>
<md-card-content>
<md-field>
<label>%i18n:@name%</label>
- <md-input v-model="name" :disabled="saving"/>
+ <md-input v-model="name" :disabled="saving" md-counter="30"/>
</md-field>
<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>
+
+ <md-field>
+ <md-icon>%fa:map-marker-alt%</md-icon>
<label>%i18n:@location%</label>
<md-input v-model="location" :disabled="saving"/>
</md-field>
<md-field>
+ <md-icon>%fa:birthday-cake%</md-icon>
+ <label>%i18n:@birthday%</label>
+ <md-input type="date" v-model="birthday" :disabled="saving"/>
+ </md-field>
+
+ <md-field>
<label>%i18n:@description%</label>
- <md-textarea v-model="description" :disabled="saving"/>
+ <md-textarea v-model="description" :disabled="saving" md-counter="500"/>
</md-field>
<md-field>
- <label>%i18n:@birthday%</label>
- <md-input type="date" v-model="birthday" :disabled="saving"/>
+ <label>%i18n:@avatar%</label>
+ <md-file @md-change="onAvatarChange"/>
</md-field>
- <div>
- <div class="md-body-2">%i18n:@avatar%</div>
- <md-menu md-direction="bottom-end" :md-close-on-select="true">
- <md-button md-menu-trigger>%i18n:@set-avatar%</md-button>
- <md-menu-content>
- <md-menu-item @click="uploadAvatar">%i18n:@upload-avatar%</md-menu-item>
- <md-menu-item @click="chooseAvatar">%i18n:@choose-avatar%</md-menu-item>
- </md-menu-content>
- </md-menu>
- </div>
+ <md-field>
+ <label>%i18n:@banner%</label>
+ <md-file @md-change="onBannerChange"/>
+ </md-field>
+
+ <md-dialog-alert
+ :md-active.sync="uploading"
+ md-content="%18n:!@uploading%"/>
<div>
- <div class="md-body-2">%i18n:@banner%</div>
- <md-menu md-direction="bottom-end" :md-close-on-select="true">
- <md-button md-menu-trigger>%i18n:@set-banner%</md-button>
- <md-menu-content>
- <md-menu-item @click="uploadAvatar">%i18n:@upload-banner%</md-menu-item>
- <md-menu-item @click="chooseAvatar">%i18n:@choose-banner%</md-menu-item>
- </md-menu-content>
- </md-menu>
+ <md-switch v-model="isCat">%i18n:@is-cat%</md-switch>
</div>
</md-card-content>
@@ -56,58 +61,83 @@
<script lang="ts">
import Vue from 'vue';
+import { apiUrl, host } from '../../../../config';
export default Vue.extend({
data() {
return {
+ host,
name: null,
+ username: null,
location: null,
description: null,
birthday: null,
- saving: false
+ avatarId: null,
+ bannerId: null,
+ isBot: false,
+ isCat: false,
+ saving: false,
+ uploading: false
};
},
+
created() {
this.name = (this as any).os.i.name || '';
+ this.username = (this as any).os.i.username;
this.location = (this as any).os.i.profile.location;
this.description = (this as any).os.i.description;
this.birthday = (this as any).os.i.profile.birthday;
+ this.avatarId = (this as any).os.i.avatarId;
+ this.bannerId = (this as any).os.i.bannerId;
+ this.isBot = (this as any).os.i.isBot;
+ this.isCat = (this as any).os.i.isCat;
},
+
methods: {
- chooseAvatar() {
- (this as any).apis.chooseDriveFile({
- multiple: false
- }).then(file => {
- this.avatarSaving = true;
+ onAvatarChange([file]) {
+ this.uploading = true;
+
+ const data = new FormData();
+ data.append('file', file);
+ data.append('i', (this as any).os.i.token);
- (this as any).api('i/update', {
- avatarId: file.id
- }).then(() => {
- this.avatarSaving = false;
- alert('%i18n:!@avatar-saved%');
- });
+ fetch(apiUrl + '/drive/files/create', {
+ method: 'POST',
+ body: data
+ })
+ .then(response => response.json())
+ .then(f => {
+ this.avatarId = f.id;
+ this.uploading = false;
+ })
+ .catch(e => {
+ this.uploading = false;
+ alert('%18n:!@upload-failed%');
});
},
- chooseBanner() {
- (this as any).apis.chooseDriveFile({
- multiple: false
- }).then(file => {
- this.bannerSaving = true;
- (this as any).api('i/update', {
- bannerId: file.id
- }).then(() => {
- this.bannerSaving = false;
- alert('%i18n:!@banner-saved%');
- });
+ onBannerChange([file]) {
+ this.uploading = true;
+
+ const data = new FormData();
+ data.append('file', file);
+ data.append('i', (this as any).os.i.token);
+
+ fetch(apiUrl + '/drive/files/create', {
+ method: 'POST',
+ body: data
+ })
+ .then(response => response.json())
+ .then(f => {
+ this.bannerId = f.id;
+ this.uploading = false;
+ })
+ .catch(e => {
+ this.uploading = false;
+ alert('%18n:!@upload-failed%');
});
},
- uploadAvatar() {
- // a
- },
- uploadBanner() {
- // a
- },
+
save() {
this.saving = true;
@@ -115,10 +145,19 @@ export default Vue.extend({
name: this.name || null,
location: this.location || null,
description: this.description || null,
- birthday: this.birthday || null
- }).then(() => {
+ birthday: this.birthday || null,
+ avatarId: this.avatarId,
+ bannerId: this.bannerId,
+ isBot: this.isBot,
+ isCat: this.isCat
+ }).then(i => {
this.saving = false;
- alert('%i18n:!@saved%');
+ (this as any).os.i.avatarId = i.avatarId;
+ (this as any).os.i.avatarUrl = i.avatarUrl;
+ (this as any).os.i.bannerId = i.bannerId;
+ (this as any).os.i.bannerUrl = i.bannerUrl;
+
+ alert('%i18n:@saved%');
});
}
}
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 34adeb03cd..84fd7eda02 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -1,7 +1,7 @@
<template>
<mk-ui>
<template slot="header" v-if="!fetching"><img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">{{ user | userName }}</template>
- <main v-if="!fetching" :data-darkmode="_darkmode_">
+ <main v-if="!fetching" :data-darkmode="$store.state.device.darkmode">
<div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div>
<div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div>
<header>
diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue
index f0a0877862..03abcabe8f 100644
--- a/src/client/app/mobile/views/pages/widgets.vue
+++ b/src/client/app/mobile/views/pages/widgets.vue
@@ -65,7 +65,7 @@ export default Vue.extend({
computed: {
widgets(): any[] {
- return this.$store.state.settings.data.mobileHome;
+ return this.$store.state.settings.mobileHome;
}
},
diff --git a/src/client/app/reset.styl b/src/client/app/reset.styl
index 10bd3113a2..c0a88f27b0 100644
--- a/src/client/app/reset.styl
+++ b/src/client/app/reset.styl
@@ -1,3 +1,6 @@
+input
+ min-width 0px
+
input:not([type])
input[type='text']
input[type='password']
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index 1f1189054d..e300d31d8d 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -1,4 +1,6 @@
import Vuex from 'vuex';
+import createPersistedState from 'vuex-persistedstate';
+
import MiOS from './mios';
const defaultSettings = {
@@ -11,17 +13,36 @@ const defaultSettings = {
gradientWindowHeader: false,
showReplyTarget: true,
showMyRenotes: true,
- showRenotedMyNotes: true
+ showRenotedMyNotes: true,
+ loadRemoteMedia: true,
+ disableViaMobile: false
+};
+
+const defaultDeviceSettings = {
+ apiViaStream: true,
+ autoPopout: false,
+ darkmode: false,
+ enableSounds: true,
+ soundVolume: 0.5,
+ lang: null,
+ preventUpdate: false,
+ debug: false,
+ lightmode: false,
+ loadRawImages: false,
+ postStyle: 'standard'
};
export default (os: MiOS) => new Vuex.Store({
plugins: [store => {
store.subscribe((mutation, state) => {
if (mutation.type.startsWith('settings/')) {
- localStorage.setItem('settings', JSON.stringify(state.settings.data));
+ localStorage.setItem('settings', JSON.stringify(state.settings));
}
});
- }],
+ }, createPersistedState({
+ paths: ['device'],
+ filter: mut => mut.type.startsWith('device/')
+ })],
state: {
indicate: false,
@@ -39,50 +60,60 @@ export default (os: MiOS) => new Vuex.Store({
},
modules: {
+ device: {
+ namespaced: true,
+
+ state: defaultDeviceSettings,
+
+ mutations: {
+ set(state, x: { key: string; value: any }) {
+ state[x.key] = x.value;
+ }
+ }
+ },
+
settings: {
namespaced: true,
- state: {
- data: defaultSettings
- },
+ state: defaultSettings,
mutations: {
set(state, x: { key: string; value: any }) {
- state.data[x.key] = x.value;
+ state[x.key] = x.value;
},
setHome(state, data) {
- state.data.home = data;
+ state.home = data;
},
setHomeWidget(state, x) {
- const w = state.data.home.find(w => w.id == x.id);
+ const w = state.home.find(w => w.id == x.id);
if (w) {
w.data = x.data;
}
},
addHomeWidget(state, widget) {
- state.data.home.unshift(widget);
+ state.home.unshift(widget);
},
setMobileHome(state, data) {
- state.data.mobileHome = data;
+ state.mobileHome = data;
},
setMobileHomeWidget(state, x) {
- const w = state.data.mobileHome.find(w => w.id == x.id);
+ const w = state.mobileHome.find(w => w.id == x.id);
if (w) {
w.data = x.data;
}
},
addMobileHomeWidget(state, widget) {
- state.data.mobileHome.unshift(widget);
+ state.mobileHome.unshift(widget);
},
removeMobileHomeWidget(state, widget) {
- state.data.mobileHome = state.data.mobileHome.filter(w => w.id != widget.id);
+ state.mobileHome = state.mobileHome.filter(w => w.id != widget.id);
}
},
@@ -108,7 +139,7 @@ export default (os: MiOS) => new Vuex.Store({
ctx.commit('addHomeWidget', widget);
os.api('i/update_home', {
- home: ctx.state.data.home
+ home: ctx.state.home
});
},
@@ -116,7 +147,7 @@ export default (os: MiOS) => new Vuex.Store({
ctx.commit('addMobileHomeWidget', widget);
os.api('i/update_mobile_home', {
- home: ctx.state.data.mobileHome
+ home: ctx.state.mobileHome
});
},
@@ -124,7 +155,7 @@ export default (os: MiOS) => new Vuex.Store({
ctx.commit('removeMobileHomeWidget', widget);
os.api('i/update_mobile_home', {
- home: ctx.state.data.mobileHome.filter(w => w.id != widget.id)
+ home: ctx.state.mobileHome.filter(w => w.id != widget.id)
});
}
}
diff --git a/src/client/assets/title.svg b/src/client/assets/title.svg
deleted file mode 100644
index 747fcd38b1..0000000000
--- a/src/client/assets/title.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
- y="0px" width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
-<circle fill="#2B2F2D" cx="128" cy="153.6" r="19.201"/>
-<circle fill="#2B2F2D" cx="51.2" cy="153.6" r="19.2"/>
-<circle fill="#2B2F2D" cx="204.8" cy="153.6" r="19.2"/>
-<polyline fill="none" stroke="#2B2F2D" stroke-width="16" stroke-linejoin="round" stroke-miterlimit="10" points="51.2,153.6
- 89.601,102.4 128,153.6 166.4,102.4 204.799,153.6 "/>
-<circle fill="#2B2F2D" cx="89.6" cy="102.4" r="19.2"/>
-<circle fill="#2B2F2D" cx="166.4" cy="102.4" r="19.199"/>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-</svg>
diff --git a/src/client/assets/version.html b/src/client/assets/version.html
index d8a98279a6..177d37db8f 100644
--- a/src/client/assets/version.html
+++ b/src/client/assets/version.html
@@ -10,11 +10,6 @@
localStorage.setItem('v', v);
}
- const lang = window.prompt('Enter language (optional):');
- if (lang && lang.length > 0) {
- localStorage.setItem('lang', lang);
- }
-
setTimeout(() => {
location.href = '/';
}, 500);
diff --git a/src/client/md.scss b/src/client/md.scss
index d850863efd..8368365885 100644
--- a/src/client/md.scss
+++ b/src/client/md.scss
@@ -6,7 +6,7 @@
@include md-register-theme("default", (
primary: $themeColor,
- accent: md-get-palette-color(red, A200)
+ accent: $themeColor
));
@import "~vue-material/dist/components/MdButton/theme";
diff --git a/src/config/types.ts b/src/config/types.ts
index dff3f7d37c..910c03c2c1 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -41,6 +41,8 @@ export type Source = {
secret_key: string;
};
+ preventCacheRemoteFiles: boolean;
+
/**
* ゴーストアカウントのID
*/
diff --git a/src/index.ts b/src/index.ts
index d633fcbbcb..bcd6561691 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -194,7 +194,12 @@ cluster.on('exit', worker => {
// Display detail of unhandled promise rejection
process.on('unhandledRejection', console.dir);
+// Display detail of uncaught exception
+process.on('uncaughtException', err => {
+ console.error(err);
+});
+
// Dying away...
-process.on('exit', () => {
- Logger.info('The process is going exit');
+process.on('exit', code => {
+ Logger.info(`The process is going exit (${code})`);
});
diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
index 8a18567dc6..a3a567038e 100644
--- a/src/models/drive-file.ts
+++ b/src/models/drive-file.ts
@@ -32,7 +32,7 @@ export type IMetadata = {
uri?: string;
url?: string;
deletedAt?: Date;
- isExpired?: boolean;
+ isMetaOnly?: boolean;
};
export type IDriveFile = {
@@ -155,7 +155,8 @@ export const pack = (
_target = Object.assign(_target, _file.metadata);
_target.src = _file.metadata.url;
- _target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
+ _target.url = _file.metadata.isMetaOnly ? _file.metadata.url : `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
+ _target.isRemote = _file.metadata.isMetaOnly;
if (_target.properties == null) _target.properties = {};
diff --git a/src/models/note.ts b/src/models/note.ts
index 5070923363..1274901d45 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -324,6 +324,10 @@ export const pack = async (
// resolve promises in _note object
_note = await rap(_note);
+ if (_note.user.isCat && _note.text) {
+ _note.text = _note.text.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ');
+ }
+
if (hide) {
_note.mediaIds = [];
_note.text = null;
diff --git a/src/models/user.ts b/src/models/user.ts
index 108111ceca..477bb232e4 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -77,6 +77,7 @@ export interface ILocalUser extends IUserBase {
};
lastUsedAt: Date;
isBot: boolean;
+ isCat: boolean;
isPro: boolean;
twoFactorSecret: string;
twoFactorEnabled: boolean;
diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts
index 7b9faf4cf4..cdb87a4114 100644
--- a/src/server/api/common/read-notification.ts
+++ b/src/server/api/common/read-notification.ts
@@ -1,6 +1,7 @@
import * as mongo from 'mongodb';
import { default as Notification, INotification } from '../../../models/notification';
import publishUserStream from '../../../publishers/stream';
+import Mute from '../../../models/mute';
/**
* Mark as read notification(s)
@@ -26,6 +27,11 @@ export default (
? [new mongo.ObjectID(message)]
: [(message as INotification)._id];
+ const mute = await Mute.find({
+ muterId: userId
+ });
+ const mutedUserIds = mute.map(m => m.muteeId);
+
// Update documents
await Notification.update({
_id: { $in: ids },
@@ -42,6 +48,9 @@ export default (
const count = await Notification
.count({
notifieeId: userId,
+ notifierId: {
+ $nin: mutedUserIds
+ },
isRead: false
}, {
limit: 1
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index 7647c76d3d..892da3540f 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -482,7 +482,7 @@ const endpoints: Endpoint[] = [
name: 'notes/replies'
},
{
- name: 'notes/context'
+ name: 'notes/conversation'
},
{
name: 'notes/create',
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
index e9348e4e2f..dd748d6bba 100644
--- a/src/server/api/endpoints/drive/files/create.ts
+++ b/src/server/api/endpoints/drive/files/create.ts
@@ -1,6 +1,7 @@
/**
* Module dependencies
*/
+import * as fs from 'fs';
import $ from 'cafy'; import ID from '../../../../../cafy-id';
import { validateFileName, pack } from '../../../../../models/drive-file';
import create from '../../../../../services/drive/add-file';
@@ -32,15 +33,23 @@ module.exports = async (file, params, user): Promise<any> => {
const [folderId = null, folderIdErr] = $.type(ID).optional().nullable().get(params.folderId);
if (folderIdErr) throw 'invalid folderId param';
+ function cleanup() {
+ fs.unlink(file.path, () => {});
+ }
+
try {
// Create file
const driveFile = await create(user, file.path, name, null, folderId);
+ cleanup();
+
// Serialize
return pack(driveFile);
} catch (e) {
console.error(e);
+ cleanup();
+
throw e;
}
};
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index 50ed9b27e8..ba9c47508c 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -96,8 +96,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
});
// Serialize
- res(await Promise.all(notifications.map(async notification =>
- await pack(notification))));
+ res(await Promise.all(notifications.map(notification => pack(notification))));
// Mark as read all
if (notifications.length > 0 && markAsRead) {
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index b7b25d0f65..6e0c5b8515 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -47,6 +47,11 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => {
if (isBotErr) return rej('invalid isBot param');
if (isBot != null) user.isBot = isBot;
+ // Get 'isCat' parameter
+ const [isCat, isCatErr] = $.bool.optional().get(params.isCat);
+ if (isCatErr) return rej('invalid isCat param');
+ if (isCat != null) user.isCat = isCat;
+
// Get 'autoWatch' parameter
const [autoWatch, autoWatchErr] = $.bool.optional().get(params.autoWatch);
if (autoWatchErr) return rej('invalid autoWatch param');
@@ -82,6 +87,7 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => {
bannerColor: user.bannerColor,
profile: user.profile,
isBot: user.isBot,
+ isCat: user.isCat,
settings: user.settings
}
});
diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts
index 4ce7613d70..21946d1bd3 100644
--- a/src/server/api/endpoints/notes.ts
+++ b/src/server/api/endpoints/notes.ts
@@ -8,6 +8,10 @@ import Note, { pack } from '../../../models/note';
* Get all notes
*/
module.exports = (params) => new Promise(async (res, rej) => {
+ // Get 'local' parameter
+ const [local, localErr] = $.bool.optional().get(params.local);
+ if (localErr) return rej('invalid local param');
+
// Get 'reply' parameter
const [reply, replyErr] = $.bool.optional().get(params.reply);
if (replyErr) return rej('invalid reply param');
@@ -61,6 +65,10 @@ module.exports = (params) => new Promise(async (res, rej) => {
};
}
+ if (local) {
+ query['_user.host'] = null;
+ }
+
if (reply != undefined) {
query.replyId = reply ? { $exists: true, $ne: null } : null;
}
diff --git a/src/server/api/endpoints/notes/context.ts b/src/server/api/endpoints/notes/conversation.ts
index 1cd27250e2..02f7229ccf 100644
--- a/src/server/api/endpoints/notes/context.ts
+++ b/src/server/api/endpoints/notes/conversation.ts
@@ -5,11 +5,7 @@ import $ from 'cafy'; import ID from '../../../../cafy-id';
import Note, { pack } from '../../../../models/note';
/**
- * Show a context of a note
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
+ * Show conversation of a note
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
@@ -33,7 +29,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
return rej('note not found');
}
- const context = [];
+ const conversation = [];
let i = 0;
async function get(id) {
@@ -41,10 +37,10 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const p = await Note.findOne({ _id: id });
if (i > offset) {
- context.push(p);
+ conversation.push(p);
}
- if (context.length == limit) {
+ if (conversation.length == limit) {
return;
}
@@ -58,6 +54,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
}
// Serialize
- res(await Promise.all(context.map(async note =>
- await pack(note, user))));
+ res(await Promise.all(conversation.map(note => pack(note, user))));
});
diff --git a/src/server/api/endpoints/notifications/get_unread_count.ts b/src/server/api/endpoints/notifications/get_unread_count.ts
index 600a80d194..9766366ff1 100644
--- a/src/server/api/endpoints/notifications/get_unread_count.ts
+++ b/src/server/api/endpoints/notifications/get_unread_count.ts
@@ -9,8 +9,7 @@ import Mute from '../../../../models/mute';
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
const mute = await Mute.find({
- muterId: user._id,
- deletedAt: { $exists: false }
+ muterId: user._id
});
const mutedUserIds = mute.map(m => m.muteeId);
diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts
index d613a3aa5f..e04400317f 100644
--- a/src/server/file/send-drive-file.ts
+++ b/src/server/file/send-drive-file.ts
@@ -33,11 +33,12 @@ export default async function(ctx: Koa.Context) {
if (file.metadata.deletedAt) {
ctx.status = 410;
- if (file.metadata.isExpired) {
- await send(ctx, '/cache-expired.png', { root: assets });
- } else {
- await send(ctx, '/tombstone.png', { root: assets });
- }
+ await send(ctx, '/tombstone.png', { root: assets });
+ return;
+ }
+
+ if (file.metadata.isMetaOnly) {
+ ctx.status = 204;
return;
}
diff --git a/src/server/index.ts b/src/server/index.ts
index ded8f7706e..fc3d252e10 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -4,8 +4,7 @@
import * as fs from 'fs';
import * as http from 'http';
-import * as https from 'https';
-//import * as http2 from 'http2';
+import * as http2 from 'http2';
import * as zlib from 'zlib';
import * as Koa from 'koa';
import * as Router from 'koa-router';
@@ -68,8 +67,7 @@ function createServer() {
certs[k] = fs.readFileSync(config.https[k]);
});
certs['allowHTTP1'] = true;
- //return http2.createSecureServer(certs, app.callback());
- return https.createServer(certs, app.callback());
+ return http2.createSecureServer(certs, app.callback());
} else {
return http.createServer(app.callback());
}
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 6ceef17c1c..5ce040d083 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -49,8 +49,8 @@ const router = new Router();
//#region static assets
router.get('/assets/*', async ctx => {
- // 無圧縮スクリプトを用意するのは大変なので一時的に無効化
- const path = process.env.NODE_ENV == 'production' ? ctx.path.replace('raw.js', 'min.js') : ctx.path.replace('min.js', 'raw.js');
+ // 互換性のため
+ const path = ctx.path.replace('.raw.js', '.js').replace('.min.js', '.js');
await send(ctx, path, {
root: client,
maxage: ms('7 days'),
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index bcd5bee512..0e42a00bf6 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -1,6 +1,5 @@
import { Buffer } from 'buffer';
import * as fs from 'fs';
-import * as tmp from 'tmp';
import * as stream from 'stream';
import * as mongodb from 'mongodb';
@@ -14,8 +13,7 @@ import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile, DriveFileChunk }
import DriveFolder from '../../models/drive-folder';
import { pack } from '../../models/drive-file';
import event, { publishDriveStream } from '../../publishers/stream';
-import getAcct from '../../acct/render';
-import { IUser, isLocalUser, isRemoteUser } from '../../models/user';
+import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
import DriveFileThumbnail, { getDriveFileThumbnailBucket, DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
import genThumbnail from '../../drive/gen-thumbnail';
@@ -25,13 +23,6 @@ const gm = _gm.subClass({
const log = debug('misskey:drive:add-file');
-const tmpFile = (): Promise<[string, any]> => new Promise((resolve, reject) => {
- tmp.file((e, path, fd, cleanup) => {
- if (e) return reject(e);
- resolve([path, cleanup]);
- });
-});
-
const writeChunks = (name: string, readable: stream.Readable, type: string, metadata: any) =>
getDriveFileBucket()
.then(bucket => new Promise((resolve, reject) => {
@@ -55,64 +46,115 @@ const writeThumbnailChunks = (name: string, readable: stream.Readable, originalI
readable.pipe(writeStream);
}));
-const addFile = async (
+async function deleteOldFile(user: IRemoteUser) {
+ const oldFile = await DriveFile.findOne({
+ _id: {
+ $nin: [user.avatarId, user.bannerId]
+ }
+ }, {
+ sort: {
+ _id: 1
+ }
+ });
+
+ 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
+ }
+}
+
+/**
+ * Add file to drive
+ *
+ * @param user User who wish to add file
+ * @param path File path
+ * @param name Name
+ * @param comment Comment
+ * @param folderId Folder ID
+ * @param force If set to true, forcibly upload the file even if there is a file with the same hash.
+ * @return Created drive file
+ */
+export default async function(
user: IUser,
path: string,
name: string = null,
comment: string = null,
folderId: mongodb.ObjectID = null,
force: boolean = false,
+ metaOnly: boolean = false,
url: string = null,
uri: string = null
-): Promise<IDriveFile> => {
- log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`);
+): Promise<IDriveFile> {
+ // Calc md5 hash
+ const calcHash = new Promise<string>((res, rej) => {
+ const readable = fs.createReadStream(path);
+ const hash = crypto.createHash('md5');
+ const chunks = [];
+ readable
+ .on('error', rej)
+ .pipe(hash)
+ .on('error', rej)
+ .on('data', chunk => chunks.push(chunk))
+ .on('end', () => {
+ const buffer = Buffer.concat(chunks);
+ res(buffer.toString('hex'));
+ });
+ });
- // Calculate hash, get content type and get file size
- const [hash, [mime, ext], size] = await Promise.all([
- // hash
- ((): Promise<string> => new Promise((res, rej) => {
- const readable = fs.createReadStream(path);
- const hash = crypto.createHash('md5');
- const chunks = [];
- readable
- .on('error', rej)
- .pipe(hash)
- .on('error', rej)
- .on('data', (chunk) => chunks.push(chunk))
- .on('end', () => {
- const buffer = Buffer.concat(chunks);
- res(buffer.toString('hex'));
- });
- }))(),
- // mime
- ((): Promise<[string, string | null]> => new Promise((res, rej) => {
- const readable = fs.createReadStream(path);
- readable
- .on('error', rej)
- .once('data', (buffer: Buffer) => {
- readable.destroy();
- const type = fileType(buffer);
- if (type) {
- return res([type.mime, type.ext]);
- } else {
- // 種類が同定できなかったら application/octet-stream にする
- return res(['application/octet-stream', null]);
- }
- });
- }))(),
- // size
- ((): Promise<number> => new Promise((res, rej) => {
- fs.stat(path, (err, stats) => {
- if (err) return rej(err);
- res(stats.size);
+ // Detect content type
+ const detectMime = new Promise<[string, string]>((res, rej) => {
+ const readable = fs.createReadStream(path);
+ readable
+ .on('error', rej)
+ .once('data', (buffer: Buffer) => {
+ readable.destroy();
+ const type = fileType(buffer);
+ if (type) {
+ res([type.mime, type.ext]);
+ } else {
+ // 種類が同定できなかったら application/octet-stream にする
+ res(['application/octet-stream', null]);
+ }
});
- }))()
- ]);
+ });
+
+ // Get file size
+ const getFileSize = new Promise<number>((res, rej) => {
+ fs.stat(path, (err, stats) => {
+ if (err) return rej(err);
+ res(stats.size);
+ });
+ });
+
+ const [hash, [mime, ext], size] = await Promise.all([calcHash, detectMime, getFileSize]);
log(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`);
// detect name
- const detectedName: string = name || (ext ? `untitled.${ext}` : 'untitled');
+ const detectedName = name || (ext ? `untitled.${ext}` : 'untitled');
if (!force) {
// Check if there is a file with the same hash
@@ -125,26 +167,72 @@ const addFile = async (
if (much !== null) {
log('file with same hash is found');
return much;
- } else {
- log('file with same hash is not found');
}
}
- const [wh, averageColor, folder] = await Promise.all([
- // Width and height (when image)
- (async () => {
- // 画像かどうか
- if (!/^image\/.*$/.test(mime)) {
- return null;
- }
+ //#region Check drive usage
+ if (!metaOnly) {
+ const usage = await DriveFile
+ .aggregate([{
+ $match: {
+ 'metadata.userId': user._id,
+ 'metadata.deletedAt': { $exists: false }
+ }
+ }, {
+ $project: {
+ length: true
+ }
+ }, {
+ $group: {
+ _id: null,
+ usage: { $sum: '$length' }
+ }
+ }])
+ .then((aggregates: any[]) => {
+ if (aggregates.length > 0) {
+ return aggregates[0].usage;
+ }
+ return 0;
+ });
- const imageType = mime.split('/')[1];
+ log(`drive usage is ${usage}`);
- // 画像でもPNGかJPEGかGIFでないならスキップ
- if (imageType != 'png' && imageType != 'jpeg' && imageType != 'gif') {
- return null;
+ // If usage limit exceeded
+ if (usage + size > user.driveCapacity) {
+ if (isLocalUser(user)) {
+ throw 'no-free-space';
+ } else {
+ // (アバターまたはバナーを含まず)最も古いファイルを削除する
+ deleteOldFile(user);
}
+ }
+ }
+ //#endregion
+
+ const fetchFolder = async () => {
+ if (!folderId) {
+ return null;
+ }
+ const driveFolder = await DriveFolder.findOne({
+ _id: folderId,
+ userId: user._id
+ });
+
+ if (driveFolder == null) throw 'folder-not-found';
+
+ return driveFolder;
+ };
+
+ const properties = {};
+
+ let propPromises = [];
+
+ const isImage = ['image/jpeg', 'image/gif', 'image/png'].includes(mime);
+
+ if (isImage) {
+ // Calc width and height
+ const calcWh = async () => {
log('calculate image width and height...');
// Calculate width and height
@@ -153,22 +241,12 @@ const addFile = async (
log(`image width and height is calculated: ${size.width}, ${size.height}`);
- return [size.width, size.height];
- })(),
- // average color (when image)
- (async () => {
- // 画像かどうか
- if (!/^image\/.*$/.test(mime)) {
- return null;
- }
-
- const imageType = mime.split('/')[1];
-
- // 画像でもPNGかJPEGでないならスキップ
- if (imageType != 'png' && imageType != 'jpeg') {
- return null;
- }
+ properties['width'] = size.width;
+ properties['height'] = size.height;
+ };
+ // Calc average color
+ const calcAvg = async () => {
log('calculate average color...');
const info = await prominence(gm(fs.createReadStream(path), name)).identify();
@@ -185,111 +263,15 @@ const addFile = async (
log(`average color is calculated: ${r}, ${g}, ${b}`);
- return isTransparent ? [r, g, b, 255] : [r, g, b];
- })(),
- // folder
- (async () => {
- if (!folderId) {
- return null;
- }
- const driveFolder = await DriveFolder.findOne({
- _id: folderId,
- userId: user._id
- });
- if (!driveFolder) {
- throw 'folder-not-found';
- }
- return driveFolder;
- })(),
- // usage checker
- (async () => {
- // Calculate drive usage
- const usage = await DriveFile
- .aggregate([{
- $match: {
- 'metadata.userId': user._id,
- 'metadata.deletedAt': { $exists: false }
- }
- }, {
- $project: {
- length: true
- }
- }, {
- $group: {
- _id: null,
- usage: { $sum: '$length' }
- }
- }])
- .then((aggregates: any[]) => {
- if (aggregates.length > 0) {
- return aggregates[0].usage;
- }
- return 0;
- });
-
- log(`drive usage is ${usage}`);
-
- // If usage limit exceeded
- if (usage + size > user.driveCapacity) {
- if (isLocalUser(user)) {
- throw 'no-free-space';
- } else {
- //#region (アバターまたはバナーを含まず)最も古いファイルを削除する
- const oldFile = await DriveFile.findOne({
- _id: {
- $nin: [user.avatarId, user.bannerId]
- }
- }, {
- sort: {
- _id: 1
- }
- });
-
- 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
- }
- //#endregion
- }
- }
- })()
- ]);
-
- const readable = fs.createReadStream(path);
+ const value = isTransparent ? [r, g, b, 255] : [r, g, b];
- const properties = {};
+ properties['avgColor'] = value;
+ };
- if (wh) {
- properties['width'] = wh[0];
- properties['height'] = wh[1];
+ propPromises = [calcWh(), calcAvg()];
}
- if (averageColor) {
- properties['avgColor'] = averageColor;
- }
+ const [folder] = await Promise.all([fetchFolder(), propPromises]);
const metadata = {
userId: user._id,
@@ -298,7 +280,8 @@ const addFile = async (
},
folderId: folder !== null ? folder._id : null,
comment: comment,
- properties: properties
+ properties: properties,
+ isMetaOnly: metaOnly
} as IMetadata;
if (url !== null) {
@@ -309,74 +292,35 @@ const addFile = async (
metadata.uri = uri;
}
- const file = await (writeChunks(detectedName, readable, mime, metadata) as Promise<IDriveFile>);
-
- try {
- const thumb = await genThumbnail(file);
- if (thumb) {
- await writeThumbnailChunks(detectedName, thumb, file._id);
- }
- } catch (e) {
- // noop
- }
+ const driveFile = metaOnly
+ ? await DriveFile.insert({
+ length: 0,
+ uploadDate: new Date(),
+ md5: hash,
+ filename: detectedName,
+ metadata: metadata,
+ contentType: mime
+ })
+ : await (writeChunks(detectedName, fs.createReadStream(path), mime, metadata) as Promise<IDriveFile>);
- return file;
-};
+ log(`drive file has been created ${driveFile._id}`);
-/**
- * Add file to drive
- *
- * @param user User who wish to add file
- * @param file File path or readableStream
- * @param comment Comment
- * @param type File type
- * @param folderId Folder ID
- * @param force If set to true, forcibly upload the file even if there is a file with the same hash.
- * @return Object that represents added file
- */
-export default (user: any, file: string | stream.Readable, ...args) => new Promise<any>((resolve, reject) => {
- const isStream = typeof file === 'object' && typeof file.read === 'function';
+ pack(driveFile).then(packedFile => {
+ // Publish drive_file_created event
+ event(user._id, 'drive_file_created', packedFile);
+ publishDriveStream(user._id, 'file_created', packedFile);
+ });
- // Get file path
- new Promise<[string, any]>((res, rej) => {
- if (typeof file === 'string') {
- res([file, null]);
- } else if (isStream) {
- tmpFile()
- .then(([path, cleanup]) => {
- const readable: stream.Readable = file;
- const writable = fs.createWriteStream(path);
- readable
- .on('error', rej)
- .on('end', () => {
- res([path, cleanup]);
- })
- .pipe(writable)
- .on('error', rej);
- })
- .catch(rej);
- } else {
- rej(new Error('un-compatible file.'));
+ if (!metaOnly) {
+ try {
+ const thumb = await genThumbnail(driveFile);
+ if (thumb) {
+ await writeThumbnailChunks(detectedName, thumb, driveFile._id);
+ }
+ } catch (e) {
+ // noop
}
- })
- .then(([path, cleanup]) => new Promise<IDriveFile>((res, rej) => {
- addFile(user, path, ...args)
- .then(file => {
- res(file);
- if (cleanup) cleanup();
- })
- .catch(rej);
- }))
- .then(file => {
- log(`drive file has been created ${file._id}`);
-
- resolve(file);
+ }
- pack(file).then(packedFile => {
- // Publish drive_file_created event
- event(user._id, 'drive_file_created', packedFile);
- publishDriveStream(user._id, 'file_created', packedFile);
- });
- })
- .catch(reject);
-});
+ return driveFile;
+}
diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts
index ad2620c036..e216ca603d 100644
--- a/src/services/drive/upload-from-url.ts
+++ b/src/services/drive/upload-from-url.ts
@@ -1,14 +1,17 @@
+import * as fs from 'fs';
import * as URL from 'url';
-import { IDriveFile, validateFileName } from '../../models/drive-file';
-import create from './add-file';
+
import * as debug from 'debug';
import * as tmp from 'tmp';
-import * as fs from 'fs';
import * as request from 'request';
+import { IDriveFile, validateFileName } from '../../models/drive-file';
+import create from './add-file';
+import config from '../../config';
+
const log = debug('misskey:drive:upload-from-url');
-export default async (url, user, folderId = null, uri = null): Promise<IDriveFile> => {
+export default async (url: string, user, folderId = null, uri: string = null): Promise<IDriveFile> => {
log(`REQUESTED: ${url}`);
let name = URL.parse(url).pathname.split('/').pop();
@@ -43,7 +46,7 @@ export default async (url, user, folderId = null, uri = null): Promise<IDriveFil
let error;
try {
- driveFile = await create(user, path, name, null, folderId, false, url, uri);
+ driveFile = await create(user, path, name, null, folderId, false, config.preventCacheRemoteFiles, url, uri);
log(`created: ${driveFile._id}`);
} catch (e) {
error = e;
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index f049c34b65..b9ff1f679b 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -283,6 +283,8 @@ export default async (user: IUser, data: {
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;