summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/app/auth/views/form.vue4
-rw-r--r--src/client/app/auth/views/index.vue2
-rw-r--r--src/client/app/base.pug12
-rw-r--r--src/client/app/boot.js35
-rw-r--r--src/client/app/common/define-widget.ts62
-rw-r--r--src/client/app/common/scripts/check-for-update.ts4
-rw-r--r--src/client/app/common/scripts/streaming/channel.ts2
-rw-r--r--src/client/app/common/scripts/streaming/drive.ts2
-rw-r--r--src/client/app/common/scripts/streaming/global-timeline.ts2
-rw-r--r--src/client/app/common/scripts/streaming/home.ts25
-rw-r--r--src/client/app/common/scripts/streaming/local-timeline.ts2
-rw-r--r--src/client/app/common/scripts/streaming/messaging-index.ts2
-rw-r--r--src/client/app/common/scripts/streaming/messaging.ts2
-rw-r--r--src/client/app/common/scripts/streaming/othello-game.ts2
-rw-r--r--src/client/app/common/scripts/streaming/othello.ts2
-rw-r--r--src/client/app/common/scripts/streaming/server.ts2
-rw-r--r--src/client/app/common/scripts/streaming/stream.ts2
-rw-r--r--src/client/app/common/scripts/streaming/user-list.ts17
-rw-r--r--src/client/app/common/views/components/autocomplete.vue12
-rw-r--r--src/client/app/common/views/components/avatar.vue42
-rw-r--r--src/client/app/common/views/components/google.vue67
-rw-r--r--src/client/app/common/views/components/index.ts2
-rw-r--r--src/client/app/common/views/components/media-list.vue17
-rw-r--r--src/client/app/common/views/components/messaging-room.message.vue32
-rw-r--r--src/client/app/common/views/components/messaging-room.vue16
-rw-r--r--src/client/app/common/views/components/messaging.vue45
-rw-r--r--src/client/app/common/views/components/nav.vue10
-rw-r--r--src/client/app/common/views/components/note-html.ts21
-rw-r--r--src/client/app/common/views/components/note-menu.vue12
-rw-r--r--src/client/app/common/views/components/othello.vue20
-rw-r--r--src/client/app/common/views/components/poll-editor.vue10
-rw-r--r--src/client/app/common/views/components/poll.vue17
-rw-r--r--src/client/app/common/views/components/reaction-picker.vue21
-rw-r--r--src/client/app/common/views/components/reactions-viewer.vue33
-rw-r--r--src/client/app/common/views/components/signin.vue4
-rw-r--r--src/client/app/common/views/components/signup.vue4
-rw-r--r--src/client/app/common/views/components/stream-indicator.vue2
-rw-r--r--src/client/app/common/views/components/switch.vue25
-rw-r--r--src/client/app/common/views/components/twitter-setting.vue2
-rw-r--r--src/client/app/common/views/components/url-preview.vue127
-rw-r--r--src/client/app/common/views/components/visibility-chooser.vue223
-rw-r--r--src/client/app/common/views/components/welcome-timeline.vue17
-rw-r--r--src/client/app/common/views/widgets/access-log.vue3
-rw-r--r--src/client/app/common/views/widgets/broadcast.vue1
-rw-r--r--src/client/app/common/views/widgets/calendar.vue21
-rw-r--r--src/client/app/common/views/widgets/donation.vue18
-rw-r--r--src/client/app/common/views/widgets/nav.vue29
-rw-r--r--src/client/app/common/views/widgets/photo-stream.vue2
-rw-r--r--src/client/app/common/views/widgets/rss.vue69
-rw-r--r--src/client/app/common/views/widgets/server.cpu-memory.vue11
-rw-r--r--src/client/app/common/views/widgets/server.cpu.vue10
-rw-r--r--src/client/app/common/views/widgets/server.disk.vue10
-rw-r--r--src/client/app/common/views/widgets/server.memory.vue10
-rw-r--r--src/client/app/common/views/widgets/server.pie.vue10
-rw-r--r--src/client/app/common/views/widgets/server.vue2
-rw-r--r--src/client/app/common/views/widgets/slideshow.vue6
-rw-r--r--src/client/app/common/views/widgets/tips.vue2
-rw-r--r--src/client/app/desktop/api/update-avatar.ts2
-rw-r--r--src/client/app/desktop/api/update-banner.ts4
-rw-r--r--src/client/app/desktop/script.ts5
-rw-r--r--src/client/app/desktop/style.styl20
-rw-r--r--src/client/app/desktop/ui.styl56
-rw-r--r--src/client/app/desktop/views/components/activity.calendar.vue2
-rw-r--r--src/client/app/desktop/views/components/activity.vue76
-rw-r--r--src/client/app/desktop/views/components/calendar.vue24
-rw-r--r--src/client/app/desktop/views/components/context-menu.menu.vue16
-rw-r--r--src/client/app/desktop/views/components/context-menu.vue12
-rw-r--r--src/client/app/desktop/views/components/dialog.vue2
-rw-r--r--src/client/app/desktop/views/components/drive.file.vue14
-rw-r--r--src/client/app/desktop/views/components/drive.vue18
-rw-r--r--src/client/app/desktop/views/components/ellipsis-icon.vue2
-rw-r--r--src/client/app/desktop/views/components/follow-button.vue28
-rw-r--r--src/client/app/desktop/views/components/friends-maker.vue17
-rw-r--r--src/client/app/desktop/views/components/home.vue104
-rw-r--r--src/client/app/desktop/views/components/index.ts2
-rw-r--r--src/client/app/desktop/views/components/media-image-dialog.vue2
-rw-r--r--src/client/app/desktop/views/components/media-image.vue14
-rw-r--r--src/client/app/desktop/views/components/media-video-dialog.vue2
-rw-r--r--src/client/app/desktop/views/components/media-video.vue1
-rw-r--r--src/client/app/desktop/views/components/mentions.vue8
-rw-r--r--src/client/app/desktop/views/components/note-detail.sub.vue59
-rw-r--r--src/client/app/desktop/views/components/note-detail.vue112
-rw-r--r--src/client/app/desktop/views/components/note-preview.vue39
-rw-r--r--src/client/app/desktop/views/components/notes.note.sub.vue69
-rw-r--r--src/client/app/desktop/views/components/notes.note.vue352
-rw-r--r--src/client/app/desktop/views/components/notes.vue249
-rw-r--r--src/client/app/desktop/views/components/notifications.vue331
-rw-r--r--src/client/app/desktop/views/components/post-form.vue200
-rw-r--r--src/client/app/desktop/views/components/renote-form.vue59
-rw-r--r--src/client/app/desktop/views/components/repost-form.vue131
-rw-r--r--src/client/app/desktop/views/components/settings.api.vue2
-rw-r--r--src/client/app/desktop/views/components/settings.vue103
-rw-r--r--src/client/app/desktop/views/components/sub-note-content.vue1
-rw-r--r--src/client/app/desktop/views/components/timeline.core.vue136
-rw-r--r--src/client/app/desktop/views/components/timeline.vue117
-rw-r--r--src/client/app/desktop/views/components/ui.header.account.vue76
-rw-r--r--src/client/app/desktop/views/components/ui.header.nav.vue12
-rw-r--r--src/client/app/desktop/views/components/ui.header.notifications.vue21
-rw-r--r--src/client/app/desktop/views/components/ui.header.search.vue4
-rw-r--r--src/client/app/desktop/views/components/ui.header.vue13
-rw-r--r--src/client/app/desktop/views/components/user-list-timeline.vue93
-rw-r--r--src/client/app/desktop/views/components/user-lists-window.vue69
-rw-r--r--src/client/app/desktop/views/components/user-preview.vue38
-rw-r--r--src/client/app/desktop/views/components/users-list.item.vue17
-rw-r--r--src/client/app/desktop/views/components/users-list.vue8
-rw-r--r--src/client/app/desktop/views/components/widget-container.vue33
-rw-r--r--src/client/app/desktop/views/components/window.vue57
-rw-r--r--src/client/app/desktop/views/pages/favorites.vue73
-rw-r--r--src/client/app/desktop/views/pages/note.vue21
-rw-r--r--src/client/app/desktop/views/pages/search.vue2
-rw-r--r--src/client/app/desktop/views/pages/user-list.users.vue124
-rw-r--r--src/client/app/desktop/views/pages/user-list.vue71
-rw-r--r--src/client/app/desktop/views/pages/user/user.followers-you-know.vue4
-rw-r--r--src/client/app/desktop/views/pages/user/user.friends.vue21
-rw-r--r--src/client/app/desktop/views/pages/user/user.header.vue61
-rw-r--r--src/client/app/desktop/views/pages/user/user.home.vue4
-rw-r--r--src/client/app/desktop/views/pages/user/user.photos.vue4
-rw-r--r--src/client/app/desktop/views/pages/user/user.profile.vue69
-rw-r--r--src/client/app/desktop/views/pages/user/user.timeline.vue73
-rw-r--r--src/client/app/desktop/views/pages/welcome.vue27
-rw-r--r--src/client/app/desktop/views/widgets/activity.vue2
-rw-r--r--src/client/app/desktop/views/widgets/channel.vue5
-rw-r--r--src/client/app/desktop/views/widgets/messaging.vue33
-rw-r--r--src/client/app/desktop/views/widgets/notifications.vue55
-rw-r--r--src/client/app/desktop/views/widgets/polls.vue69
-rw-r--r--src/client/app/desktop/views/widgets/post-form.vue5
-rw-r--r--src/client/app/desktop/views/widgets/profile.vue34
-rw-r--r--src/client/app/desktop/views/widgets/timemachine.vue1
-rw-r--r--src/client/app/desktop/views/widgets/trends.vue104
-rw-r--r--src/client/app/desktop/views/widgets/users.vue165
-rw-r--r--src/client/app/init.css7
-rw-r--r--src/client/app/init.ts58
-rw-r--r--src/client/app/mios.ts (renamed from src/client/app/common/mios.ts)73
-rw-r--r--src/client/app/mobile/script.ts8
-rw-r--r--src/client/app/mobile/style.styl4
-rw-r--r--src/client/app/mobile/views/components/drive-file-chooser.vue2
-rw-r--r--src/client/app/mobile/views/components/drive-folder-chooser.vue2
-rw-r--r--src/client/app/mobile/views/components/drive.file-detail.vue4
-rw-r--r--src/client/app/mobile/views/components/drive.vue6
-rw-r--r--src/client/app/mobile/views/components/friends-maker.vue2
-rw-r--r--src/client/app/mobile/views/components/index.ts4
-rw-r--r--src/client/app/mobile/views/components/media-image.vue12
-rw-r--r--src/client/app/mobile/views/components/note-card.vue14
-rw-r--r--src/client/app/mobile/views/components/note-detail.sub.vue44
-rw-r--r--src/client/app/mobile/views/components/note-detail.vue127
-rw-r--r--src/client/app/mobile/views/components/note-preview.vue39
-rw-r--r--src/client/app/mobile/views/components/note.sub.vue75
-rw-r--r--src/client/app/mobile/views/components/note.vue392
-rw-r--r--src/client/app/mobile/views/components/notes.vue257
-rw-r--r--src/client/app/mobile/views/components/notification.vue139
-rw-r--r--src/client/app/mobile/views/components/notifications.vue55
-rw-r--r--src/client/app/mobile/views/components/post-form.vue186
-rw-r--r--src/client/app/mobile/views/components/sub-note-content.vue1
-rw-r--r--src/client/app/mobile/views/components/timeline.vue113
-rw-r--r--src/client/app/mobile/views/components/ui.header.vue18
-rw-r--r--src/client/app/mobile/views/components/ui.nav.vue50
-rw-r--r--src/client/app/mobile/views/components/user-list-timeline.vue93
-rw-r--r--src/client/app/mobile/views/components/user-preview.vue25
-rw-r--r--src/client/app/mobile/views/components/user-timeline.vue73
-rw-r--r--src/client/app/mobile/views/components/users-list.vue14
-rw-r--r--src/client/app/mobile/views/components/widget-container.vue2
-rw-r--r--src/client/app/mobile/views/pages/dashboard.vue195
-rw-r--r--src/client/app/mobile/views/pages/followers.vue3
-rw-r--r--src/client/app/mobile/views/pages/following.vue3
-rw-r--r--src/client/app/mobile/views/pages/home.timeline.vue149
-rw-r--r--src/client/app/mobile/views/pages/home.vue308
-rw-r--r--src/client/app/mobile/views/pages/note.vue44
-rw-r--r--src/client/app/mobile/views/pages/notifications.vue23
-rw-r--r--src/client/app/mobile/views/pages/profile-setting.vue5
-rw-r--r--src/client/app/mobile/views/pages/search.vue3
-rw-r--r--src/client/app/mobile/views/pages/selectdrive.vue2
-rw-r--r--src/client/app/mobile/views/pages/settings.vue5
-rw-r--r--src/client/app/mobile/views/pages/signup.vue2
-rw-r--r--src/client/app/mobile/views/pages/user.vue100
-rw-r--r--src/client/app/mobile/views/pages/user/home.vue27
-rw-r--r--src/client/app/mobile/views/pages/welcome.vue231
-rw-r--r--src/client/app/mobile/views/widgets/activity.vue1
-rw-r--r--src/client/app/mobile/views/widgets/profile.vue6
-rw-r--r--src/client/app/store.ts92
-rw-r--r--src/client/docs/api/endpoints/view.pug2
-rw-r--r--src/client/docs/api/entities/note.yaml6
-rw-r--r--src/client/docs/api/entities/post.yaml6
-rw-r--r--src/client/docs/api/entities/view.pug2
-rw-r--r--src/client/docs/api/mixins.pug4
-rw-r--r--src/client/docs/follow.ja.pug9
185 files changed, 5142 insertions, 3010 deletions
diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue
index b323907eb0..152b900429 100644
--- a/src/client/app/auth/views/form.vue
+++ b/src/client/app/auth/views/form.vue
@@ -94,13 +94,13 @@ export default Vue.extend({
margin 0 auto -38px auto
border solid 5px #fff
border-radius 100%
- box-shadow 0 2px 2px rgba(0, 0, 0, 0.1)
+ box-shadow 0 2px 2px rgba(#000, 0.1)
> .app
padding 44px 16px 0 16px
color #555
background #eee
- box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) inset
+ box-shadow 0 2px 2px rgba(#000, 0.1) inset
&:after
content ''
diff --git a/src/client/app/auth/views/index.vue b/src/client/app/auth/views/index.vue
index e1e1b265e1..0fcd9bfe53 100644
--- a/src/client/app/auth/views/index.vue
+++ b/src/client/app/auth/views/index.vue
@@ -94,7 +94,7 @@ export default Vue.extend({
margin 0 auto
text-align center
background #fff
- box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
+ box-shadow 0px 4px 16px rgba(#000, 0.2)
> .fetching
margin 0
diff --git a/src/client/app/base.pug b/src/client/app/base.pug
index 32a95a6c99..c182fd6f64 100644
--- a/src/client/app/base.pug
+++ b/src/client/app/base.pug
@@ -1,3 +1,5 @@
+block vars
+
doctype html
!= '\n<!-- Thank you for using Misskey! @syuilo -->\n'
@@ -9,9 +11,17 @@ html
meta(name='application-name' content='Misskey')
meta(name='theme-color' content=themeColor)
meta(name='referrer' content='origin')
+ meta(property='og:site_name' content='Misskey')
link(rel='manifest' href='/manifest.json')
- title Misskey
+ title
+ block title
+ | Misskey
+
+ block desc
+ meta(name='description' content='A SNS')
+
+ block meta
style
include ./../../../built/client/assets/init.css
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index a0709842b9..35d02cf9c5 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -11,14 +11,12 @@
'use strict';
-// Chromeで確認したことなのですが、constやletを用いたとしても
-// グローバルなスコープで定数/変数を定義するとwindowのプロパティ
-// としてそれがアクセスできるようになる訳ではありませんが、普通に
-// コンソールから定数/変数名を入力するとアクセスできてしまいます。
-// ブロック内に入れてスコープを非グローバル化するとそれが防げます
-// (Chrome以外のブラウザでは検証していません)
-{
- if (localStorage.getItem('shouldFlush') == 'true') refresh();
+(function() {
+ // キャッシュ削除要求があれば従う
+ if (localStorage.getItem('shouldFlush') == 'true') {
+ refresh();
+ return;
+ }
// Get the current url information
const url = new URL(location.href);
@@ -62,6 +60,11 @@
app = isMobile ? 'mobile' : 'desktop';
}
+ // Dark/Light
+ if (localStorage.getItem('darkmode') == 'true') {
+ document.documentElement.setAttribute('data-darkmode', 'true');
+ }
+
// Script version
const ver = localStorage.getItem('v') || VERSION;
@@ -72,11 +75,16 @@
const raw = (localStorage.getItem('useRawScript') == 'true' && isDebug)
|| ENV != 'production';
+ // Get salt query
+ const salt = localStorage.getItem('salt')
+ ? '?salt=' + localStorage.getItem('salt')
+ : '';
+
// Load an app script
// 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`);
+ script.setAttribute('src', `/assets/${app}.${ver}.${lang}.${raw ? 'raw' : 'min'}.js${salt}`);
script.setAttribute('async', 'true');
script.setAttribute('defer', 'true');
head.appendChild(script);
@@ -97,8 +105,8 @@
const meta = await res.json();
// Compare versions
- if (meta.version != ver) {
- localStorage.setItem('v', meta.version);
+ if (meta.clientVersion != ver) {
+ localStorage.setItem('v', meta.clientVersion);
alert(
'Misskeyの新しいバージョンがあります。ページを再度読み込みします。' +
@@ -112,6 +120,9 @@
function refresh() {
localStorage.setItem('shouldFlush', 'false');
+ // Random
+ localStorage.setItem('salt', Math.random().toString());
+
// Clear cache (serive worker)
try {
navigator.serviceWorker.controller.postMessage('clear');
@@ -126,4 +137,4 @@
// Force reload
location.reload(true);
}
-}
+})();
diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts
index 7b98c0903f..0b2bc36566 100644
--- a/src/client/app/common/define-widget.ts
+++ b/src/client/app/common/define-widget.ts
@@ -18,61 +18,65 @@ export default function<T extends object>(data: {
default: false
}
},
+
computed: {
id(): string {
return this.widget.id;
+ },
+
+ props(): T {
+ return this.widget.data;
}
},
+
data() {
return {
- props: data.props ? data.props() : {} as T,
- bakedOldProps: null,
- preventSave: false
+ bakedOldProps: null
};
},
+
created() {
- if (this.props) {
- Object.keys(this.props).forEach(prop => {
- if (this.widget.data.hasOwnProperty(prop)) {
- this.props[prop] = this.widget.data[prop];
- }
- });
- }
+ this.mergeProps();
+
+ this.$watch('props', () => {
+ this.mergeProps();
+ });
this.bakeProps();
+ },
+
+ methods: {
+ bakeProps() {
+ this.bakedOldProps = JSON.stringify(this.props);
+ },
- this.$watch('props', newProps => {
- if (this.preventSave) {
- this.preventSave = false;
- this.bakeProps();
- return;
+ mergeProps() {
+ if (data.props) {
+ const defaultProps = data.props();
+ Object.keys(defaultProps).forEach(prop => {
+ if (!this.props.hasOwnProperty(prop)) {
+ Vue.set(this.props, prop, defaultProps[prop]);
+ }
+ });
}
- if (this.bakedOldProps == JSON.stringify(newProps)) return;
+ },
+
+ save() {
+ if (this.bakedOldProps == JSON.stringify(this.props)) return;
this.bakeProps();
if (this.isMobile) {
(this as any).api('i/update_mobile_home', {
id: this.id,
- data: newProps
- }).then(() => {
- (this as any).os.i.clientSettings.mobileHome.find(w => w.id == this.id).data = newProps;
+ data: this.props
});
} else {
(this as any).api('i/update_home', {
id: this.id,
- data: newProps
- }).then(() => {
- (this as any).os.i.clientSettings.home.find(w => w.id == this.id).data = newProps;
+ data: this.props
});
}
- }, {
- deep: true
- });
- },
- methods: {
- bakeProps() {
- this.bakedOldProps = JSON.stringify(this.props);
}
}
});
diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts
index 20ce64ea85..1e303017eb 100644
--- a/src/client/app/common/scripts/check-for-update.ts
+++ b/src/client/app/common/scripts/check-for-update.ts
@@ -1,9 +1,9 @@
-import MiOS from '../mios';
+import MiOS from '../../mios';
import { version as current } from '../../config';
export default async function(mios: MiOS, force = false, silent = false) {
const meta = await mios.getMeta(force);
- const newer = meta.version;
+ const newer = meta.clientVersion;
if (newer != current) {
localStorage.setItem('should-refresh', 'true');
diff --git a/src/client/app/common/scripts/streaming/channel.ts b/src/client/app/common/scripts/streaming/channel.ts
index cab5f4edb4..be68ec0997 100644
--- a/src/client/app/common/scripts/streaming/channel.ts
+++ b/src/client/app/common/scripts/streaming/channel.ts
@@ -1,5 +1,5 @@
import Stream from './stream';
-import MiOS from '../../mios';
+import MiOS from '../../../mios';
/**
* Channel stream connection
diff --git a/src/client/app/common/scripts/streaming/drive.ts b/src/client/app/common/scripts/streaming/drive.ts
index 7ff85b5946..50fff05737 100644
--- a/src/client/app/common/scripts/streaming/drive.ts
+++ b/src/client/app/common/scripts/streaming/drive.ts
@@ -1,6 +1,6 @@
import Stream from './stream';
import StreamManager from './stream-manager';
-import MiOS from '../../mios';
+import MiOS from '../../../mios';
/**
* Drive stream connection
diff --git a/src/client/app/common/scripts/streaming/global-timeline.ts b/src/client/app/common/scripts/streaming/global-timeline.ts
index 452ddbac03..a639f1595c 100644
--- a/src/client/app/common/scripts/streaming/global-timeline.ts
+++ b/src/client/app/common/scripts/streaming/global-timeline.ts
@@ -1,6 +1,6 @@
import Stream from './stream';
import StreamManager from './stream-manager';
-import MiOS from '../../mios';
+import MiOS from '../../../mios';
/**
* Global timeline stream connection
diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts
index 73f2c5302c..32685f3c2c 100644
--- a/src/client/app/common/scripts/streaming/home.ts
+++ b/src/client/app/common/scripts/streaming/home.ts
@@ -2,7 +2,7 @@ import * as merge from 'object-assign-deep';
import Stream from './stream';
import StreamManager from './stream-manager';
-import MiOS from '../../mios';
+import MiOS from '../../../mios';
/**
* Home stream connection
@@ -25,10 +25,31 @@ export class HomeStream extends Stream {
console.log('I updated:', i);
}
merge(me, i);
+
+ // キャッシュ更新
+ os.bakeMe();
+ });
+
+ this.on('clientSettingUpdated', x => {
+ os.store.commit('settings/set', {
+ key: x.key,
+ value: x.value
+ });
+ });
+
+ this.on('home_updated', x => {
+ if (x.home) {
+ os.store.commit('settings/setHome', x.home);
+ } else {
+ os.store.commit('settings/setHomeWidget', {
+ id: x.id,
+ data: x.data
+ });
+ }
});
// トークンが再生成されたとき
- // このままではAPIが利用できないので強制的にサインアウトさせる
+ // このままではMisskeyが利用できないので強制的にサインアウトさせる
this.on('my_token_regenerated', () => {
alert('%i18n:!common.my-token-regenerated%');
os.signout();
diff --git a/src/client/app/common/scripts/streaming/local-timeline.ts b/src/client/app/common/scripts/streaming/local-timeline.ts
index 3d04e05cd4..2834262bdc 100644
--- a/src/client/app/common/scripts/streaming/local-timeline.ts
+++ b/src/client/app/common/scripts/streaming/local-timeline.ts
@@ -1,6 +1,6 @@
import Stream from './stream';
import StreamManager from './stream-manager';
-import MiOS from '../../mios';
+import MiOS from '../../../mios';
/**
* Local timeline stream connection
diff --git a/src/client/app/common/scripts/streaming/messaging-index.ts b/src/client/app/common/scripts/streaming/messaging-index.ts
index 84e2174ec4..addcccb952 100644
--- a/src/client/app/common/scripts/streaming/messaging-index.ts
+++ b/src/client/app/common/scripts/streaming/messaging-index.ts
@@ -1,6 +1,6 @@
import Stream from './stream';
import StreamManager from './stream-manager';
-import MiOS from '../../mios';
+import MiOS from '../../../mios';
/**
* Messaging index stream connection
diff --git a/src/client/app/common/scripts/streaming/messaging.ts b/src/client/app/common/scripts/streaming/messaging.ts
index c1b5875cfb..a59377d867 100644
--- a/src/client/app/common/scripts/streaming/messaging.ts
+++ b/src/client/app/common/scripts/streaming/messaging.ts
@@ -1,5 +1,5 @@
import Stream from './stream';
-import MiOS from '../../mios';
+import MiOS from '../../../mios';
/**
* Messaging stream connection
diff --git a/src/client/app/common/scripts/streaming/othello-game.ts b/src/client/app/common/scripts/streaming/othello-game.ts
index b85af8f72b..9e36f647bb 100644
--- a/src/client/app/common/scripts/streaming/othello-game.ts
+++ b/src/client/app/common/scripts/streaming/othello-game.ts
@@ -1,5 +1,5 @@
import Stream from './stream';
-import MiOS from '../../mios';
+import MiOS from '../../../mios';
export class OthelloGameStream extends Stream {
constructor(os: MiOS, me, game) {
diff --git a/src/client/app/common/scripts/streaming/othello.ts b/src/client/app/common/scripts/streaming/othello.ts
index f5d47431cd..8f4f217e39 100644
--- a/src/client/app/common/scripts/streaming/othello.ts
+++ b/src/client/app/common/scripts/streaming/othello.ts
@@ -1,6 +1,6 @@
import StreamManager from './stream-manager';
import Stream from './stream';
-import MiOS from '../../mios';
+import MiOS from '../../../mios';
export class OthelloStream extends Stream {
constructor(os: MiOS, me) {
diff --git a/src/client/app/common/scripts/streaming/server.ts b/src/client/app/common/scripts/streaming/server.ts
index 3d35ef4d9d..2ea4239288 100644
--- a/src/client/app/common/scripts/streaming/server.ts
+++ b/src/client/app/common/scripts/streaming/server.ts
@@ -1,6 +1,6 @@
import Stream from './stream';
import StreamManager from './stream-manager';
-import MiOS from '../../mios';
+import MiOS from '../../../mios';
/**
* Server stream connection
diff --git a/src/client/app/common/scripts/streaming/stream.ts b/src/client/app/common/scripts/streaming/stream.ts
index 3912186ad3..fefa8e5ced 100644
--- a/src/client/app/common/scripts/streaming/stream.ts
+++ b/src/client/app/common/scripts/streaming/stream.ts
@@ -2,7 +2,7 @@ import { EventEmitter } from 'eventemitter3';
import * as uuid from 'uuid';
import * as ReconnectingWebsocket from 'reconnecting-websocket';
import { wsUrl } from '../../../config';
-import MiOS from '../../mios';
+import MiOS from '../../../mios';
/**
* Misskey stream connection
diff --git a/src/client/app/common/scripts/streaming/user-list.ts b/src/client/app/common/scripts/streaming/user-list.ts
new file mode 100644
index 0000000000..30a52b98dd
--- /dev/null
+++ b/src/client/app/common/scripts/streaming/user-list.ts
@@ -0,0 +1,17 @@
+import Stream from './stream';
+import MiOS from '../../mios';
+
+export class UserListStream extends Stream {
+ constructor(os: MiOS, me, listId) {
+ super(os, 'user-list', {
+ i: me.token,
+ listId
+ });
+
+ (this as any).on('_connected_', () => {
+ this.send({
+ i: me.token
+ });
+ });
+ }
+}
diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index 5c8f61a2a2..84173d20b5 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -234,7 +234,7 @@ export default Vue.extend({
margin-top calc(1em + 8px)
overflow hidden
background #fff
- border solid 1px rgba(0, 0, 0, 0.1)
+ border solid 1px rgba(#000, 0.1)
border-radius 4px
transition top 0.1s ease, left 0.1s ease
@@ -253,7 +253,7 @@ export default Vue.extend({
white-space nowrap
overflow hidden
font-size 0.9em
- color rgba(0, 0, 0, 0.8)
+ color rgba(#000, 0.8)
cursor default
&, *
@@ -285,10 +285,10 @@ export default Vue.extend({
.name
margin 0 8px 0 0
- color rgba(0, 0, 0, 0.8)
+ color rgba(#000, 0.8)
.username
- color rgba(0, 0, 0, 0.3)
+ color rgba(#000, 0.3)
> .emojis > li
@@ -298,10 +298,10 @@ export default Vue.extend({
width 24px
.name
- color rgba(0, 0, 0, 0.8)
+ color rgba(#000, 0.8)
.alias
margin 0 0 0 8px
- color rgba(0, 0, 0, 0.3)
+ color rgba(#000, 0.3)
</style>
diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue
new file mode 100644
index 0000000000..a4648c272e
--- /dev/null
+++ b/src/client/app/common/views/components/avatar.vue
@@ -0,0 +1,42 @@
+<template>
+ <router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="style" v-if="disablePreview"></router-link>
+ <router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="style" v-else v-user-preview="user.id"></router-link>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+ props: {
+ user: {
+ type: Object,
+ required: true
+ },
+ target: {
+ required: false,
+ default: null
+ },
+ disablePreview: {
+ required: false,
+ default: false
+ }
+ },
+ computed: {
+ style(): any {
+ return {
+ backgroundColor: this.user.avatarColor ? `rgb(${ this.user.avatarColor.join(',') })` : null,
+ backgroundImage: `url(${ this.user.avatarUrl }?thumbnail)`,
+ borderRadius: (this as any).clientSettings.circleIcons ? '100%' : null
+ };
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-avatar
+ display inline-block
+ vertical-align bottom
+ background-size cover
+ background-position center center
+ transition border-radius 1s ease
+</style>
diff --git a/src/client/app/common/views/components/google.vue b/src/client/app/common/views/components/google.vue
new file mode 100644
index 0000000000..92817d3c1f
--- /dev/null
+++ b/src/client/app/common/views/components/google.vue
@@ -0,0 +1,67 @@
+<template>
+<div class="mk-google">
+ <input type="search" v-model="query" :placeholder="q">
+ <button @click="search">検索</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+ props: ['q'],
+ data() {
+ return {
+ query: null
+ };
+ },
+ mounted() {
+ this.query = this.q;
+ },
+ methods: {
+ search() {
+ window.open(`https://www.google.com/?#q=${this.query}`, '_blank');
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+ display flex
+ margin 8px 0
+
+ > input
+ flex-shrink 1
+ padding 10px
+ width 100%
+ height 40px
+ font-family sans-serif
+ font-size 16px
+ color isDark ? #dee4e8 : #55595c
+ background isDark ? #191b22 : #fff
+ border solid 1px isDark ? #495156 : #dadada
+ border-radius 4px 0 0 4px
+
+ &:hover
+ border-color isDark ? #777c86 : #b0b0b0
+
+ > button
+ flex-shrink 0
+ padding 0 16px
+ border solid 1px isDark ? #495156 : #dadada
+ border-left none
+ border-radius 0 4px 4px 0
+
+ &:hover
+ background-color isDark ? #2e3440 : #eee
+
+ &:active
+ box-shadow 0 2px 4px rgba(#000, 0.15) inset
+
+.mk-google[data-darkmode]
+ root(true)
+
+.mk-google:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 6bfe43a800..69fed00c74 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -3,6 +3,7 @@ import Vue from 'vue';
import signin from './signin.vue';
import signup from './signup.vue';
import forkit from './forkit.vue';
+import avatar from './avatar.vue';
import nav from './nav.vue';
import noteHtml from './note-html';
import poll from './poll.vue';
@@ -28,6 +29,7 @@ import welcomeTimeline from './welcome-timeline.vue';
Vue.component('mk-signin', signin);
Vue.component('mk-signup', signup);
Vue.component('mk-forkit', forkit);
+Vue.component('mk-avatar', avatar);
Vue.component('mk-nav', nav);
Vue.component('mk-note-html', noteHtml);
Vue.component('mk-poll', poll);
diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue
index 64172ad0b4..ff9d5e1022 100644
--- a/src/client/app/common/views/components/media-list.vue
+++ b/src/client/app/common/views/components/media-list.vue
@@ -2,7 +2,7 @@
<div class="mk-media-list" :data-count="mediaList.length">
<template v-for="media in mediaList">
<mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')" :inline-playable="mediaList.length === 1"/>
- <mk-media-image :image="media" :key="media.id" v-else />
+ <mk-media-image :image="media" :key="media.id" v-else :raw="raw"/>
</template>
</div>
</template>
@@ -11,7 +11,14 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['mediaList'],
+ props: {
+ mediaList: {
+ required: true
+ },
+ raw: {
+ default: false
+ }
+ }
});
</script>
@@ -23,7 +30,7 @@ export default Vue.extend({
@media (max-width 500px)
height 192px
-
+
&[data-count="1"]
grid-template-rows 1fr
&[data-count="2"]
@@ -40,7 +47,7 @@ export default Vue.extend({
&[data-count="4"]
grid-template-columns 1fr 1fr
grid-template-rows 1fr 1fr
-
+
:nth-child(1)
grid-column 1 / 2
grid-row 1 / 2
@@ -53,5 +60,5 @@ export default Vue.extend({
:nth-child(4)
grid-column 2 / 3
grid-row 2 / 3
-
+
</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 afd700e777..ba0ab3209f 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -1,8 +1,6 @@
<template>
<div class="message" :data-is-me="isMe">
- <router-link class="avatar-anchor" :to="message.user | userPage" :title="message.user | acct" target="_blank">
- <img class="avatar" :src="`${message.user.avatarUrl}?thumbnail&size=80`" alt=""/>
- </router-link>
+ <mk-avatar class="avatar" :user="message.user" target="_blank"/>
<div class="content">
<div class="balloon" :data-no-text="message.text == null">
<p class="read" v-if="isMe && message.isRead">%i18n:@is-read%</p>
@@ -67,20 +65,14 @@ export default Vue.extend({
padding 10px 12px 10px 12px
background-color transparent
- > .avatar-anchor
+ > .avatar
display block
position absolute
top 10px
-
- > .avatar
- display block
- min-width 54px
- min-height 54px
- max-width 54px
- max-height 54px
- margin 0
- border-radius 8px
- transition all 0.1s ease
+ width 54px
+ height 54px
+ border-radius 8px
+ transition all 0.1s ease
> .content
@@ -134,7 +126,7 @@ export default Vue.extend({
bottom -4px
left -12px
margin 0
- color rgba(0, 0, 0, 0.5)
+ color rgba(#000, 0.5)
font-size 11px
> .content
@@ -146,7 +138,7 @@ export default Vue.extend({
overflow hidden
overflow-wrap break-word
font-size 1em
- color rgba(0, 0, 0, 0.5)
+ color rgba(#000, 0.5)
> .text
display block
@@ -155,7 +147,7 @@ export default Vue.extend({
overflow hidden
overflow-wrap break-word
font-size 1em
- color rgba(0, 0, 0, 0.8)
+ color rgba(#000, 0.8)
& + .file
> a
@@ -195,13 +187,13 @@ export default Vue.extend({
display block
margin 2px 0 0 0
font-size 10px
- color rgba(0, 0, 0, 0.4)
+ color rgba(#000, 0.4)
> [data-fa]
margin-left 4px
&:not([data-is-me])
- > .avatar-anchor
+ > .avatar
left 12px
> .content
@@ -225,7 +217,7 @@ export default Vue.extend({
text-align left
&[data-is-me]
- > .avatar-anchor
+ > .avatar
right 12px
> .content
diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue
index 38202d7581..a45114e6bb 100644
--- a/src/client/app/common/views/components/messaging-room.vue
+++ b/src/client/app/common/views/components/messaging-room.vue
@@ -256,7 +256,7 @@ export default Vue.extend({
padding 16px 8px 8px 8px
text-align center
font-size 0.8em
- color rgba(0, 0, 0, 0.4)
+ color rgba(#000, 0.4)
[data-fa]
margin-right 4px
@@ -267,7 +267,7 @@ export default Vue.extend({
padding 16px 8px 8px 8px
text-align center
font-size 0.8em
- color rgba(0, 0, 0, 0.4)
+ color rgba(#000, 0.4)
[data-fa]
margin-right 4px
@@ -278,7 +278,7 @@ export default Vue.extend({
padding 16px
text-align center
font-size 0.8em
- color rgba(0, 0, 0, 0.4)
+ color rgba(#000, 0.4)
[data-fa]
margin-right 4px
@@ -289,14 +289,14 @@ export default Vue.extend({
padding 0 12px
line-height 24px
color #fff
- background rgba(0, 0, 0, 0.3)
+ background rgba(#000, 0.3)
border-radius 12px
&:hover
- background rgba(0, 0, 0, 0.4)
+ background rgba(#000, 0.4)
&:active
- background rgba(0, 0, 0, 0.5)
+ background rgba(#000, 0.5)
&.fetching
cursor wait
@@ -322,7 +322,7 @@ export default Vue.extend({
left 0
right 0
margin 0 auto
- background rgba(0, 0, 0, 0.1)
+ background rgba(#000, 0.1)
> span
display inline-block
@@ -330,7 +330,7 @@ export default Vue.extend({
padding 0 16px
//font-weight bold
line-height 32px
- color rgba(0, 0, 0, 0.3)
+ color rgba(#000, 0.3)
background #fff
> footer
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index f74d9643eb..11f9c366d4 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -13,7 +13,7 @@
@click="navigate(user)"
tabindex="-1"
>
- <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
+ <mk-avatar class="avatar" :user="user"/>
<span class="name">{{ user | userName }}</span>
<span class="username">@{{ user | acct }}</span>
</li>
@@ -31,7 +31,7 @@
:key="message.id"
>
<div>
- <img class="avatar" :src="`${isMe(message) ? message.recipient.avatarUrl : message.user.avatarUrl}?thumbnail&size=64`" alt=""/>
+ <mk-avatar class="avatar" :user="isMe(message) ? message.recipient : message.user"/>
<header>
<span class="name">{{ isMe(message) ? message.recipient : message.user | userName }}</span>
<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
@@ -169,7 +169,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-messaging
+root(isDark)
&[data-compact]
font-size 0.8em
@@ -205,11 +205,11 @@ export default Vue.extend({
z-index 1
width 100%
background #fff
- box-shadow 0 0px 2px rgba(0, 0, 0, 0.2)
+ box-shadow 0 0px 2px rgba(#000, 0.2)
> .form
padding 8px
- background #f7f7f7
+ background isDark ? #282c37 : #f7f7f7
> label
display block
@@ -241,13 +241,14 @@ export default Vue.extend({
line-height 38px
color #000
outline none
- border solid 1px #eee
+ background isDark ? #191b22 : #fff
+ border solid 1px isDark ? #495156 : #eee
border-radius 5px
box-shadow none
transition color 0.5s ease, border 0.5s ease
&:hover
- border solid 1px #ddd
+ border solid 1px isDark ? #b0b0b0 : #ddd
transition border 0.2s ease
&:focus
@@ -278,7 +279,7 @@ export default Vue.extend({
vertical-align top
white-space nowrap
overflow hidden
- color rgba(0, 0, 0, 0.8)
+ color rgba(#000, 0.8)
text-decoration none
transition none
cursor pointer
@@ -317,32 +318,32 @@ export default Vue.extend({
margin 0 8px 0 0
/*font-weight bold*/
font-weight normal
- color rgba(0, 0, 0, 0.8)
+ color rgba(#000, 0.8)
.username
font-weight normal
- color rgba(0, 0, 0, 0.3)
+ color rgba(#000, 0.3)
> .history
> a
display block
text-decoration none
- background #fff
- border-bottom solid 1px #eee
+ background isDark ? #282c37 : #fff
+ border-bottom solid 1px isDark ? #1c2023 : #eee
*
pointer-events none
user-select none
&:hover
- background #fafafa
+ background isDark ? #1e2129 : #fafafa
> .avatar
filter saturate(200%)
&:active
- background #eee
+ background isDark ? #14161b : #eee
&[data-is-read]
&[data-is-me]
@@ -382,17 +383,17 @@ export default Vue.extend({
overflow hidden
text-overflow ellipsis
font-size 1em
- color rgba(0, 0, 0, 0.9)
+ color isDark ? #fff : rgba(#000, 0.9)
font-weight bold
transition all 0.1s ease
> .username
margin 0 8px
- color rgba(0, 0, 0, 0.5)
+ color isDark ? #606984 : rgba(#000, 0.5)
> .mk-time
margin 0 0 0 auto
- color rgba(0, 0, 0, 0.5)
+ color isDark ? #606984 : rgba(#000, 0.5)
font-size 80%
> .avatar
@@ -412,10 +413,10 @@ export default Vue.extend({
overflow hidden
overflow-wrap break-word
font-size 1.1em
- color rgba(0, 0, 0, 0.8)
+ color isDark ? #fff : rgba(#000, 0.8)
.me
- color rgba(0, 0, 0, 0.4)
+ color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.4)
> .image
display block
@@ -460,4 +461,10 @@ export default Vue.extend({
> .avatar
margin 0 12px 0 0
+.mk-messaging[data-darkmode]
+ root(true)
+
+.mk-messaging:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/common/views/components/nav.vue b/src/client/app/common/views/components/nav.vue
index 2295957928..cd1f99288a 100644
--- a/src/client/app/common/views/components/nav.vue
+++ b/src/client/app/common/views/components/nav.vue
@@ -2,16 +2,10 @@
<span class="mk-nav">
<a :href="aboutUrl">%i18n:@about%</a>
<i>・</i>
- <a :href="statsUrl">%i18n:@stats%</a>
- <i>・</i>
- <a :href="statusUrl">%i18n:@status%</a>
- <i>・</i>
- <a href="http://zawazawa.jp/misskey/">%i18n:@wiki%</a>
- <i>・</i>
- <a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:@donors%</a>
- <i>・</i>
<a href="https://github.com/syuilo/misskey">%i18n:@repository%</a>
<i>・</i>
+ <a href="https://github.com/syuilo/misskey/issues/new" target="_blank">%i18n:@feedback%</a>
+ <i>・</i>
<a :href="devUrl">%i18n:@develop%</a>
<i>・</i>
<a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on %fa:B twitter%</a>
diff --git a/src/client/app/common/views/components/note-html.ts b/src/client/app/common/views/components/note-html.ts
index 24e750a671..f86b50659e 100644
--- a/src/client/app/common/views/components/note-html.ts
+++ b/src/client/app/common/views/components/note-html.ts
@@ -4,6 +4,7 @@ import parse from '../../../../../text/parse';
import getAcct from '../../../../../acct/render';
import { url } from '../../../config';
import MkUrl from './url.vue';
+import MkGoogle from './google.vue';
const flatten = list => list.reduce(
(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
@@ -97,7 +98,9 @@ export default Vue.component('mk-note-html', {
}, token.content);
case 'code':
- return createElement('pre', [
+ return createElement('pre', {
+ class: 'code'
+ }, [
createElement('code', {
domProps: {
innerHTML: token.html
@@ -132,10 +135,24 @@ export default Vue.component('mk-note-html', {
}, text2.replace(/\n/g, ' '));
}
+ case 'title':
+ return createElement('div', {
+ attrs: {
+ class: 'title'
+ }
+ }, token.title);
+
case 'emoji':
const emoji = emojilib.lib[token.emoji];
return createElement('span', emoji ? emoji.char : token.content);
+ case 'search':
+ return createElement(MkGoogle, {
+ props: {
+ q: token.query
+ }
+ });
+
default:
console.log('unknown ast type:', token.type);
}
@@ -144,7 +161,7 @@ export default Vue.component('mk-note-html', {
const _els = [];
els.forEach((el, i) => {
if (el.tag == 'br') {
- if (els[i - 1].tag != 'div') {
+ if (!['div', 'pre'].includes(els[i - 1].tag)) {
_els.push(el);
}
} else {
diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/app/common/views/components/note-menu.vue
index 877d2c16bb..88dc22aaf4 100644
--- a/src/client/app/common/views/components/note-menu.vue
+++ b/src/client/app/common/views/components/note-menu.vue
@@ -2,6 +2,7 @@
<div class="mk-note-menu">
<div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ compact }" ref="popover">
+ <button @click="favorite">%i18n:@favorite%</button>
<button v-if="note.userId == os.i.id" @click="pin">%i18n:@pin%</button>
<a v-if="note.uri" :href="note.uri" target="_blank">%i18n:@remote%</a>
</div>
@@ -58,6 +59,14 @@ export default Vue.extend({
});
},
+ favorite() {
+ (this as any).api('notes/favorites/create', {
+ noteId: this.note.id
+ }).then(() => {
+ this.$destroy();
+ });
+ },
+
close() {
(this.$refs.backdrop as any).style.pointerEvents = 'none';
anime({
@@ -96,7 +105,7 @@ $border-color = rgba(27, 31, 35, 0.15)
z-index 10000
width 100%
height 100%
- background rgba(0, 0, 0, 0.1)
+ background rgba(#000, 0.1)
opacity 0
> .popover
@@ -142,6 +151,7 @@ $border-color = rgba(27, 31, 35, 0.15)
> a
display block
padding 8px 16px
+ width 100%
&:hover
color $theme-color-foreground
diff --git a/src/client/app/common/views/components/othello.vue b/src/client/app/common/views/components/othello.vue
index 8f7d9dfd6a..a0971c45b4 100644
--- a/src/client/app/common/views/components/othello.vue
+++ b/src/client/app/common/views/components/othello.vue
@@ -31,7 +31,7 @@
<section v-if="invitations.length > 0">
<h2>対局の招待があります!:</h2>
<div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)">
- <img :src="`${i.parent.avatarUrl}?thumbnail&size=32`" alt="">
+ <mk-avatar class="avatar" :user="i.parent"/>
<span class="name"><b>{{ i.parent.name }}</b></span>
<span class="username">@{{ i.parent.username }}</span>
<mk-time :time="i.createdAt"/>
@@ -40,8 +40,8 @@
<section v-if="myGames.length > 0">
<h2>自分の対局</h2>
<a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`">
- <img :src="`${g.user1.avatarUrl}?thumbnail&size=32`" alt="">
- <img :src="`${g.user2.avatarUrl}?thumbnail&size=32`" alt="">
+ <mk-avatar class="avatar" :user="g.user1"/>
+ <mk-avatar class="avatar" :user="g.user2"/>
<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
<span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span>
</a>
@@ -49,8 +49,8 @@
<section v-if="games.length > 0">
<h2>みんなの対局</h2>
<a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/othello/${g.id}`">
- <img :src="`${g.user1.avatarUrl}?thumbnail&size=32`" alt="">
- <img :src="`${g.user2.avatarUrl}?thumbnail&size=32`" alt="">
+ <mk-avatar class="avatar" :user="g.user1"/>
+ <mk-avatar class="avatar" :user="g.user2"/>
<span><b>{{ g.user1.name }}</b> vs <b>{{ g.user2.name }}</b></span>
<span class="state">{{ g.isEnded ? '終了' : '進行中' }}</span>
</a>
@@ -271,8 +271,9 @@ export default Vue.extend({
&:active
background #eee
- > img
- vertical-align bottom
+ > .avatar
+ width 32px
+ height 32px
border-radius 100%
> span
@@ -301,8 +302,9 @@ export default Vue.extend({
&:active
background #eee
- > img
- vertical-align bottom
+ > .avatar
+ width 32px
+ height 32px
border-radius 100%
> span
diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue
index 189172679b..95bcba996e 100644
--- a/src/client/app/common/views/components/poll-editor.vue
+++ b/src/client/app/common/views/components/poll-editor.vue
@@ -69,7 +69,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-poll-editor
+root(isDark)
padding 8px
> .caution
@@ -102,6 +102,8 @@ export default Vue.extend({
padding 6px 8px
width 300px
font-size 14px
+ color isDark ? #fff : #000
+ background isDark ? #191b22 : #fff
border solid 1px rgba($theme-color, 0.1)
border-radius 4px
@@ -139,4 +141,10 @@ export default Vue.extend({
&:active
color darken($theme-color, 30%)
+.mk-poll-editor[data-darkmode]
+ root(true)
+
+.mk-poll-editor:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
index 1834d4ddc2..46e41cbcdb 100644
--- a/src/client/app/common/views/components/poll.vue
+++ b/src/client/app/common/views/components/poll.vue
@@ -68,7 +68,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-poll
+root(isDark)
> ul
display block
@@ -81,16 +81,17 @@ export default Vue.extend({
margin 4px 0
padding 4px 8px
width 100%
- border solid 1px #eee
+ color isDark ? #fff : #000
+ border solid 1px isDark ? #5e636f : #eee
border-radius 4px
overflow hidden
cursor pointer
&:hover
- background rgba(0, 0, 0, 0.05)
+ background rgba(#000, 0.05)
&:active
- background rgba(0, 0, 0, 0.1)
+ background rgba(#000, 0.1)
> .backdrop
position absolute
@@ -108,6 +109,8 @@ export default Vue.extend({
margin-left 4px
> p
+ color isDark ? #a3aebf : #000
+
a
color inherit
@@ -121,4 +124,10 @@ export default Vue.extend({
&:active
background transparent
+.mk-poll[data-darkmode]
+ root(true)
+
+.mk-poll:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue
index 267eeb3a14..e2c8a6ed3f 100644
--- a/src/client/app/common/views/components/reaction-picker.vue
+++ b/src/client/app/common/views/components/reaction-picker.vue
@@ -110,7 +110,7 @@ export default Vue.extend({
$border-color = rgba(27, 31, 35, 0.15)
-.mk-reaction-picker
+root(isDark)
position initial
> .backdrop
@@ -120,13 +120,14 @@ $border-color = rgba(27, 31, 35, 0.15)
z-index 10000
width 100%
height 100%
- background rgba(0, 0, 0, 0.1)
+ background isDark ? rgba(#000, 0.4) : rgba(#000, 0.1)
opacity 0
> .popover
+ $bgcolor = isDark ? #2c303c : #fff
position absolute
z-index 10001
- background #fff
+ background $bgcolor
border 1px solid $border-color
border-radius 4px
box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
@@ -159,15 +160,15 @@ $border-color = rgba(27, 31, 35, 0.15)
border-top solid $balloon-size transparent
border-left solid $balloon-size transparent
border-right solid $balloon-size transparent
- border-bottom solid $balloon-size #fff
+ border-bottom solid $balloon-size $bgcolor
> p
display block
margin 0
padding 8px 10px
font-size 14px
- color #586069
- border-bottom solid 1px #e1e4e8
+ color isDark ? #d6dce2 : #586069
+ border-bottom solid 1px isDark ? #1c2023 : #e1e4e8
> div
padding 4px
@@ -182,10 +183,16 @@ $border-color = rgba(27, 31, 35, 0.15)
border-radius 2px
&:hover
- background #eee
+ background isDark ? #252731 : #eee
&:active
background $theme-color
box-shadow inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15)
+.mk-reaction-picker[data-darkmode]
+ root(true)
+
+.mk-reaction-picker:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/common/views/components/reactions-viewer.vue b/src/client/app/common/views/components/reactions-viewer.vue
index 1afcf525d2..97cb6be17c 100644
--- a/src/client/app/common/views/components/reactions-viewer.vue
+++ b/src/client/app/common/views/components/reactions-viewer.vue
@@ -1,15 +1,15 @@
<template>
<div class="mk-reactions-viewer">
<template v-if="reactions">
- <span v-if="reactions.like"><mk-reaction-icon reaction='like'/><span>{{ reactions.like }}</span></span>
- <span v-if="reactions.love"><mk-reaction-icon reaction='love'/><span>{{ reactions.love }}</span></span>
- <span v-if="reactions.laugh"><mk-reaction-icon reaction='laugh'/><span>{{ reactions.laugh }}</span></span>
- <span v-if="reactions.hmm"><mk-reaction-icon reaction='hmm'/><span>{{ reactions.hmm }}</span></span>
- <span v-if="reactions.surprise"><mk-reaction-icon reaction='surprise'/><span>{{ reactions.surprise }}</span></span>
- <span v-if="reactions.congrats"><mk-reaction-icon reaction='congrats'/><span>{{ reactions.congrats }}</span></span>
- <span v-if="reactions.angry"><mk-reaction-icon reaction='angry'/><span>{{ reactions.angry }}</span></span>
- <span v-if="reactions.confused"><mk-reaction-icon reaction='confused'/><span>{{ reactions.confused }}</span></span>
- <span v-if="reactions.pudding"><mk-reaction-icon reaction='pudding'/><span>{{ reactions.pudding }}</span></span>
+ <span v-if="reactions.like"><mk-reaction-icon reaction="like"/><span>{{ reactions.like }}</span></span>
+ <span v-if="reactions.love"><mk-reaction-icon reaction="love"/><span>{{ reactions.love }}</span></span>
+ <span v-if="reactions.laugh"><mk-reaction-icon reaction="laugh"/><span>{{ reactions.laugh }}</span></span>
+ <span v-if="reactions.hmm"><mk-reaction-icon reaction="hmm"/><span>{{ reactions.hmm }}</span></span>
+ <span v-if="reactions.surprise"><mk-reaction-icon reaction="surprise"/><span>{{ reactions.surprise }}</span></span>
+ <span v-if="reactions.congrats"><mk-reaction-icon reaction="congrats"/><span>{{ reactions.congrats }}</span></span>
+ <span v-if="reactions.angry"><mk-reaction-icon reaction="angry"/><span>{{ reactions.angry }}</span></span>
+ <span v-if="reactions.confused"><mk-reaction-icon reaction="confused"/><span>{{ reactions.confused }}</span></span>
+ <span v-if="reactions.pudding"><mk-reaction-icon reaction="pudding"/><span>{{ reactions.pudding }}</span></span>
</template>
</div>
</template>
@@ -27,9 +27,10 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-reactions-viewer
- border-top dashed 1px #eee
- border-bottom dashed 1px #eee
+root(isDark)
+ $borderColor = isDark ? #5e6673 : #eee
+ border-top dashed 1px $borderColor
+ border-bottom dashed 1px $borderColor
margin 4px 0
&:empty
@@ -44,6 +45,12 @@ export default Vue.extend({
> span
margin-left 4px
font-size 1.2em
- color #444
+ color isDark ? #d1d5dc : #444
+
+.mk-reactions-viewer[data-darkmode]
+ root(true)
+
+.mk-reactions-viewer:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue
index 25f90a2f13..7fb9fc3fd4 100644
--- a/src/client/app/common/views/components/signin.vue
+++ b/src/client/app/common/views/components/signin.vue
@@ -91,7 +91,7 @@ export default Vue.extend({
width 100%
line-height 44px
font-size 1em
- color rgba(0, 0, 0, 0.7)
+ color rgba(#000, 0.7)
background #fff
outline none
border solid 1px #eee
@@ -117,7 +117,7 @@ export default Vue.extend({
margin -6px 0 0 0
width 100%
font-size 1.2em
- color rgba(0, 0, 0, 0.5)
+ color rgba(#000, 0.5)
outline none
border none
border-radius 0
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index 33a559ff8f..516979acd0 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -234,13 +234,13 @@ export default Vue.extend({
color #333 !important
background #fff !important
outline none
- border solid 1px rgba(0, 0, 0, 0.1)
+ border solid 1px rgba(#000, 0.1)
border-radius 4px
box-shadow 0 0 0 114514px #fff inset
transition all .3s ease
&:hover
- border-color rgba(0, 0, 0, 0.2)
+ border-color rgba(#000, 0.2)
transition all .1s ease
&:focus
diff --git a/src/client/app/common/views/components/stream-indicator.vue b/src/client/app/common/views/components/stream-indicator.vue
index 93758102de..d573db32e6 100644
--- a/src/client/app/common/views/components/stream-indicator.vue
+++ b/src/client/app/common/views/components/stream-indicator.vue
@@ -73,7 +73,7 @@ export default Vue.extend({
padding 6px 12px
font-size 0.9em
color #fff
- background rgba(0, 0, 0, 0.8)
+ background rgba(#000, 0.8)
border-radius 4px
> p
diff --git a/src/client/app/common/views/components/switch.vue b/src/client/app/common/views/components/switch.vue
index 19a4adc3de..32caab638a 100644
--- a/src/client/app/common/views/components/switch.vue
+++ b/src/client/app/common/views/components/switch.vue
@@ -87,7 +87,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-switch
+root(isDark)
display flex
margin 12px 0
cursor pointer
@@ -121,11 +121,12 @@ export default Vue.extend({
&:hover
> .label
> span
- color #2e3338
+ color isDark ? #fff : #2e3338
> .button
- background #ced2da
- border-color #ced2da
+ $color = isDark ? #15181d : #ced2da
+ background $color
+ border-color $color
> input
position absolute
@@ -147,14 +148,16 @@ export default Vue.extend({
border-radius 14px
> .button
+ $color = isDark ? #1c1f25 : #dcdfe6
+
display inline-block
margin 0
width 40px
min-width 40px
height 20px
min-height 20px
- background #dcdfe6
- border 1px solid #dcdfe6
+ background $color
+ border 1px solid $color
outline none
border-radius 10px
transition inherit
@@ -179,12 +182,18 @@ export default Vue.extend({
> span
display block
line-height 20px
- color #4a535a
+ color isDark ? #c4ccd2 : #4a535a
transition inherit
> p
margin 0
//font-size 90%
- color #9daab3
+ color isDark ? #78858e : #9daab3
+
+.mk-switch[data-darkmode]
+ root(true)
+
+.mk-switch:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/common/views/components/twitter-setting.vue b/src/client/app/common/views/components/twitter-setting.vue
index 6ca1037aba..ab07e6d09a 100644
--- a/src/client/app/common/views/components/twitter-setting.vue
+++ b/src/client/app/common/views/components/twitter-setting.vue
@@ -50,8 +50,6 @@ export default Vue.extend({
<style lang="stylus" scoped>
.mk-twitter-setting
- color #4a535a
-
.account
border solid 1px #e1e8ed
border-radius 4px
diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue
index fd25480f61..3bae6e5078 100644
--- a/src/client/app/common/views/components/url-preview.vue
+++ b/src/client/app/common/views/components/url-preview.vue
@@ -2,8 +2,8 @@
<iframe v-if="youtubeId" type="text/html" height="250"
:src="`https://www.youtube.com/embed/${youtubeId}?origin=${misskeyUrl}`"
frameborder="0"/>
-<div v-else>
- <a class="mk-url-preview" :href="url" target="_blank" :title="url" v-if="!fetching">
+<div v-else class="mk-url-preview">
+ <a :href="url" target="_blank" :title="url" v-if="!fetching">
<div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div>
<article>
<header>
@@ -45,7 +45,7 @@ export default Vue.extend({
} else if (url.hostname == 'youtu.be') {
this.youtubeId = url.pathname;
} else {
- fetch('/url?url=' + this.url).then(res => {
+ fetch('/url?url=' + encodeURIComponent(this.url)).then(res => {
res.json().then(info => {
this.title = info.title;
this.description = info.description;
@@ -65,78 +65,85 @@ export default Vue.extend({
iframe
width 100%
-.mk-url-preview
- display block
- font-size 16px
- border solid 1px #eee
- border-radius 4px
- overflow hidden
+root(isDark)
+ > a
+ display block
+ font-size 16px
+ border solid 1px isDark ? #191b1f : #eee
+ border-radius 4px
+ overflow hidden
- &:hover
- text-decoration none
- border-color #ddd
+ &:hover
+ text-decoration none
+ border-color isDark ? #4f5561 : #ddd
- > article > header > h1
- text-decoration underline
+ > article > header > h1
+ text-decoration underline
- > .thumbnail
- position absolute
- width 100px
- height 100%
- background-position center
- background-size cover
+ > .thumbnail
+ position absolute
+ width 100px
+ height 100%
+ background-position center
+ background-size cover
+
+ & + article
+ left 100px
+ width calc(100% - 100px)
- & + article
- left 100px
- width calc(100% - 100px)
+ > article
+ padding 16px
- > article
- padding 16px
+ > header
+ margin-bottom 8px
- > header
- margin-bottom 8px
+ > h1
+ margin 0
+ font-size 1em
+ color isDark ? #d6dae0 : #555
- > h1
+ > p
margin 0
- font-size 1em
- color #555
+ color isDark ? #a4aab3 : #777
+ font-size 0.8em
- > p
- margin 0
- color #777
- font-size 0.8em
+ > footer
+ margin-top 8px
+ height 16px
- > footer
- margin-top 8px
- height 16px
+ > img
+ display inline-block
+ width 16px
+ height 16px
+ margin-right 4px
+ vertical-align top
- > img
- display inline-block
- width 16px
- height 16px
- margin-right 4px
- vertical-align top
+ > p
+ display inline-block
+ margin 0
+ color isDark ? #b0b4bf : #666
+ font-size 0.8em
+ line-height 16px
+ vertical-align top
- > p
- display inline-block
- margin 0
- color #666
- font-size 0.8em
- line-height 16px
- vertical-align top
+ @media (max-width 500px)
+ font-size 8px
+ border none
- @media (max-width 500px)
- font-size 8px
- border none
+ > .thumbnail
+ width 70px
- > .thumbnail
- width 70px
+ & + article
+ left 70px
+ width calc(100% - 70px)
- & + article
- left 70px
- width calc(100% - 70px)
+ > article
+ padding 8px
- > article
- padding 8px
+.mk-url-preview[data-darkmode]
+ root(true)
+
+.mk-url-preview:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
new file mode 100644
index 0000000000..50f0877ae9
--- /dev/null
+++ b/src/client/app/common/views/components/visibility-chooser.vue
@@ -0,0 +1,223 @@
+<template>
+<div class="mk-visibility-chooser">
+ <div class="backdrop" ref="backdrop" @click="close"></div>
+ <div class="popover" :class="{ compact }" ref="popover">
+ <div @click="choose('public')" :class="{ active: v == 'public' }">
+ <div>%fa:globe%</div>
+ <div>
+ <span>公開</span>
+ </div>
+ </div>
+ <div @click="choose('home')" :class="{ active: v == 'home' }">
+ <div>%fa:home%</div>
+ <div>
+ <span>ホーム</span>
+ <span>ホームタイムラインにのみ公開</span>
+ </div>
+ </div>
+ <div @click="choose('followers')" :class="{ active: v == 'followers' }">
+ <div>%fa:unlock%</div>
+ <div>
+ <span>フォロワー</span>
+ <span>自分のフォロワーにのみ公開</span>
+ </div>
+ </div>
+ <div @click="choose('specified')" :class="{ active: v == 'specified' }">
+ <div>%fa:envelope%</div>
+ <div>
+ <span>ダイレクト</span>
+ <span>指定したユーザーにのみ公開</span>
+ </div>
+ </div>
+ <div @click="choose('private')" :class="{ active: v == 'private' }">
+ <div>%fa:lock%</div>
+ <div>
+ <span>非公開</span>
+ </div>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+
+export default Vue.extend({
+ props: ['source', 'compact', 'v'],
+ mounted() {
+ this.$nextTick(() => {
+ const popover = this.$refs.popover as any;
+
+ const rect = this.source.getBoundingClientRect();
+ const width = popover.offsetWidth;
+ const height = popover.offsetHeight;
+
+ let left;
+ let top;
+
+ if (this.compact) {
+ const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+ const y = rect.top + window.pageYOffset + (this.source.offsetHeight / 2);
+ left = (x - (width / 2));
+ top = (y - (height / 2));
+ } else {
+ const x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+ const y = rect.top + window.pageYOffset + this.source.offsetHeight;
+ left = (x - (width / 2));
+ top = y;
+ }
+
+ if (left + width > window.innerWidth) {
+ left = window.innerWidth - width;
+ }
+
+ popover.style.left = left + 'px';
+ popover.style.top = top + 'px';
+
+ anime({
+ targets: this.$refs.backdrop,
+ opacity: 1,
+ duration: 100,
+ easing: 'linear'
+ });
+
+ anime({
+ targets: this.$refs.popover,
+ opacity: 1,
+ scale: [0.5, 1],
+ duration: 500
+ });
+ });
+ },
+ methods: {
+ choose(visibility) {
+ this.$emit('chosen', visibility);
+ this.$destroy();
+ },
+ close() {
+ (this.$refs.backdrop as any).style.pointerEvents = 'none';
+ anime({
+ targets: this.$refs.backdrop,
+ opacity: 0,
+ duration: 200,
+ easing: 'linear'
+ });
+
+ (this.$refs.popover as any).style.pointerEvents = 'none';
+ anime({
+ targets: this.$refs.popover,
+ opacity: 0,
+ scale: 0.5,
+ duration: 200,
+ easing: 'easeInBack',
+ complete: () => this.$destroy()
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+$border-color = rgba(27, 31, 35, 0.15)
+
+root(isDark)
+ position initial
+
+ > .backdrop
+ position fixed
+ top 0
+ left 0
+ z-index 10000
+ width 100%
+ height 100%
+ background isDark ? rgba(#000, 0.4) : rgba(#000, 0.1)
+ opacity 0
+
+ > .popover
+ $bgcolor = isDark ? #2c303c : #fff
+ position absolute
+ z-index 10001
+ width 240px
+ padding 8px 0
+ background $bgcolor
+ border 1px solid $border-color
+ border-radius 4px
+ box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
+ transform scale(0.5)
+ opacity 0
+
+ $balloon-size = 10px
+
+ &:not(.compact)
+ margin-top $balloon-size
+ transform-origin center -($balloon-size)
+
+ &:before
+ content ""
+ display block
+ position absolute
+ top -($balloon-size * 2)
+ left s('calc(50% - %s)', $balloon-size)
+ border-top solid $balloon-size transparent
+ border-left solid $balloon-size transparent
+ border-right solid $balloon-size transparent
+ border-bottom solid $balloon-size $border-color
+
+ &:after
+ content ""
+ display block
+ position absolute
+ top -($balloon-size * 2) + 1.5px
+ left s('calc(50% - %s)', $balloon-size)
+ border-top solid $balloon-size transparent
+ border-left solid $balloon-size transparent
+ border-right solid $balloon-size transparent
+ border-bottom solid $balloon-size $bgcolor
+
+ > div
+ display flex
+ padding 8px 14px
+ font-size 12px
+ color isDark ? #fff : #666
+ cursor pointer
+
+ &:hover
+ background isDark ? #252731 : #eee
+
+ &:active
+ background isDark ? #21242b : #ddd
+
+ &.active
+ color $theme-color-foreground
+ background $theme-color
+
+ > *
+ user-select none
+ pointer-events none
+
+ > *:first-child
+ display flex
+ justify-content center
+ align-items center
+ margin-right 10px
+
+ > *:last-child
+ flex 1 1 auto
+
+ > span:first-child
+ display block
+ font-weight bold
+
+ > span:last-child:not(:first-child)
+ opacity 0.6
+
+.mk-visibility-chooser[data-darkmode]
+ root(true)
+
+.mk-visibility-chooser:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index a80bc04f7f..6fadb030c3 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -1,9 +1,7 @@
<template>
<div class="mk-welcome-timeline">
<div v-for="note in notes">
- <router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.user.id">
- <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
- </router-link>
+ <mk-avatar class="avatar" :user="note.user" target="_blank"/>
<div class="body">
<header>
<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
@@ -62,25 +60,22 @@ export default Vue.extend({
overflow-wrap break-word
font-size .9em
color #4C4C4C
- border-bottom 1px solid rgba(0, 0, 0, 0.05)
+ border-bottom 1px solid rgba(#000, 0.05)
&:after
content ""
display block
clear both
- > .avatar-anchor
+ > .avatar
display block
float left
position -webkit-sticky
position sticky
top 16px
-
- > img
- display block
- width 42px
- height 42px
- border-radius 6px
+ width 42px
+ height 42px
+ border-radius 6px
> .body
float right
diff --git a/src/client/app/common/views/widgets/access-log.vue b/src/client/app/common/views/widgets/access-log.vue
index 637ba328c6..8652e35645 100644
--- a/src/client/app/common/views/widgets/access-log.vue
+++ b/src/client/app/common/views/widgets/access-log.vue
@@ -61,6 +61,7 @@ export default define({
} else {
this.props.design++;
}
+ this.save();
}
}
});
@@ -78,7 +79,7 @@ export default define({
color #555
&:nth-child(odd)
- background rgba(0, 0, 0, 0.025)
+ background rgba(#000, 0.025)
> b
margin-right 4px
diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue
index 96d1d0ef3a..75b1d60524 100644
--- a/src/client/app/common/views/widgets/broadcast.vue
+++ b/src/client/app/common/views/widgets/broadcast.vue
@@ -68,6 +68,7 @@ export default define({
} else {
this.props.design++;
}
+ this.save();
}
}
});
diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue
index 03f69a7597..41e9253784 100644
--- a/src/client/app/common/views/widgets/calendar.vue
+++ b/src/client/app/common/views/widgets/calendar.vue
@@ -73,6 +73,7 @@ export default define({
} else {
this.props.design++;
}
+ this.save();
},
tick() {
const now = new Date();
@@ -109,11 +110,11 @@ export default define({
<style lang="stylus" scoped>
@import '~const.styl'
-.mkw-calendar
+root(isDark)
padding 16px 0
- color #777
- background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
+ color isDark ? #c5ced6 :#777
+ background isDark ? #282C37 : #fff
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
&[data-special='on-new-years-day']
@@ -126,7 +127,7 @@ export default define({
&[data-mobile]
border none
border-radius 8px
- box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+ box-shadow 0 0 0 1px rgba(#000, 0.2)
&:after
content ""
@@ -171,7 +172,7 @@ export default define({
margin 0 0 2px 0
font-size 12px
line-height 18px
- color #888
+ color isDark ? #7a8692 : #888
> b
margin-left 2px
@@ -179,7 +180,7 @@ export default define({
> .meter
width 100%
overflow hidden
- background #eee
+ background isDark ? #1c1f25 : #eee
border-radius 8px
> .val
@@ -198,4 +199,10 @@ export default define({
> .meter > .val
background #41ddde
+.mkw-calendar[data-darkmode]
+ root(true)
+
+.mkw-calendar:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/common/views/widgets/donation.vue b/src/client/app/common/views/widgets/donation.vue
index 6b5a6697ed..e35462611d 100644
--- a/src/client/app/common/views/widgets/donation.vue
+++ b/src/client/app/common/views/widgets/donation.vue
@@ -19,9 +19,9 @@ export default define({
</script>
<style lang="stylus" scoped>
-.mkw-donation
- background #fff
- border solid 1px #ead8bb
+root(isDark)
+ background isDark ? #282c37 : #fff
+ border solid 1px isDark ? #c3831c : #ead8bb
border-radius 6px
> article
@@ -30,7 +30,7 @@ export default define({
> h1
margin 0 0 5px 0
font-size 1em
- color #888
+ color isDark ? #b2bac1 : #888
> [data-fa]
margin-right 0.25em
@@ -40,13 +40,13 @@ export default define({
z-index 1
margin 0
font-size 0.8em
- color #999
+ color isDark ? #a1a6ab : #999
&[data-mobile]
border none
background #ead8bb
border-radius 8px
- box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+ box-shadow 0 0 0 1px rgba(#000, 0.2)
> article
> h1
@@ -55,4 +55,10 @@ export default define({
> p
color #777d71
+.mkw-donation[data-darkmode]
+ root(true)
+
+.mkw-donation:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/common/views/widgets/nav.vue b/src/client/app/common/views/widgets/nav.vue
index 7bd5a7832f..0cbf7c158e 100644
--- a/src/client/app/common/views/widgets/nav.vue
+++ b/src/client/app/common/views/widgets/nav.vue
@@ -1,7 +1,7 @@
<template>
<div class="mkw-nav">
<mk-widget-container>
- <div :class="$style.body">
+ <div class="mkw-nav--body">
<mk-nav/>
</div>
</mk-widget-container>
@@ -15,17 +15,24 @@ export default define({
});
</script>
-<style lang="stylus" module>
-.body
- padding 16px
- font-size 12px
- color #aaa
- background #fff
+<style lang="stylus" scoped>
+root(isDark)
+ .mkw-nav--body
+ padding 16px
+ font-size 12px
+ color isDark ? #9aa4b3 : #aaa
+ background isDark ? #282c37 : #fff
- a
- color #999
+ a
+ color isDark ? #9aa4b3 : #999
- i
- color #ccc
+ i
+ color isDark ? #9aa4b3 : #ccc
+
+.mkw-nav[data-darkmode]
+ root(true)
+
+.mkw-nav:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/common/views/widgets/photo-stream.vue b/src/client/app/common/views/widgets/photo-stream.vue
index c51d932bd1..ae5924bb10 100644
--- a/src/client/app/common/views/widgets/photo-stream.vue
+++ b/src/client/app/common/views/widgets/photo-stream.vue
@@ -59,6 +59,8 @@ export default define({
} else {
this.props.design++;
}
+
+ this.save();
}
}
});
diff --git a/src/client/app/common/views/widgets/rss.vue b/src/client/app/common/views/widgets/rss.vue
index 4d74b2f7a4..b5339add0b 100644
--- a/src/client/app/common/views/widgets/rss.vue
+++ b/src/client/app/common/views/widgets/rss.vue
@@ -4,9 +4,11 @@
<template slot="header">%fa:rss-square%RSS</template>
<button slot="func" title="設定" @click="setting">%fa:cog%</button>
- <p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
- <div :class="$style.feed" v-else>
- <a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a>
+ <div class="mkw-rss--body">
+ <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+ <div class="feed" v-else>
+ <a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a>
+ </div>
</div>
</mk-widget-container>
</div>
@@ -38,6 +40,7 @@ export default define({
methods: {
func() {
this.props.compact = !this.props.compact;
+ this.save();
},
fetch() {
fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.url}`, {
@@ -56,38 +59,46 @@ export default define({
});
</script>
-<style lang="stylus" module>
-.feed
- padding 12px 16px
- font-size 0.9em
+<style lang="stylus" scoped>
+root(isDark)
+ .mkw-rss--body
+ .feed
+ padding 12px 16px
+ font-size 0.9em
- > a
- display block
- padding 4px 0
- color #666
- border-bottom dashed 1px #eee
+ > a
+ display block
+ padding 4px 0
+ color isDark ? #9aa4b3 : #666
+ border-bottom dashed 1px isDark ? #1c2023 : #eee
- &:last-child
- border-bottom none
+ &:last-child
+ border-bottom none
-.fetching
- margin 0
- padding 16px
- text-align center
- color #aaa
+ .fetching
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
- > [data-fa]
- margin-right 4px
+ > [data-fa]
+ margin-right 4px
-&[data-mobile]
- .feed
- padding 0
- font-size 1em
+ &[data-mobile]
+ .feed
+ padding 0
+ font-size 1em
- > a
- padding 8px 16px
+ > a
+ padding 8px 16px
- &:nth-child(even)
- background rgba(0, 0, 0, 0.05)
+ &:nth-child(even)
+ background rgba(#000, 0.05)
+
+.mkw-rss[data-darkmode]
+ root(true)
+
+.mkw-rss:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/common/views/widgets/server.cpu-memory.vue b/src/client/app/common/views/widgets/server.cpu-memory.vue
index d75a142568..fbd36b255a 100644
--- a/src/client/app/common/views/widgets/server.cpu-memory.vue
+++ b/src/client/app/common/views/widgets/server.cpu-memory.vue
@@ -100,7 +100,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.cpu-memory
+root(isDark)
> svg
display block
padding 10px
@@ -115,7 +115,7 @@ export default Vue.extend({
> text
font-size 5px
- fill rgba(0, 0, 0, 0.55)
+ fill isDark ? rgba(#fff, 0.55) : rgba(#000, 0.55)
> tspan
opacity 0.5
@@ -124,4 +124,11 @@ export default Vue.extend({
content ""
display block
clear both
+
+.cpu-memory[data-darkmode]
+ root(true)
+
+.cpu-memory:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/common/views/widgets/server.cpu.vue b/src/client/app/common/views/widgets/server.cpu.vue
index 596c856da8..b9748bdf7c 100644
--- a/src/client/app/common/views/widgets/server.cpu.vue
+++ b/src/client/app/common/views/widgets/server.cpu.vue
@@ -38,7 +38,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.cpu
+root(isDark)
> .pie
padding 10px
height 100px
@@ -52,7 +52,7 @@ export default Vue.extend({
> p
margin 0
font-size 12px
- color #505050
+ color isDark ? #a8b4bd : #505050
&:first-child
font-weight bold
@@ -65,4 +65,10 @@ export default Vue.extend({
display block
clear both
+.cpu[data-darkmode]
+ root(true)
+
+.cpu:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/common/views/widgets/server.disk.vue b/src/client/app/common/views/widgets/server.disk.vue
index 2af1982a96..5c7e9678de 100644
--- a/src/client/app/common/views/widgets/server.disk.vue
+++ b/src/client/app/common/views/widgets/server.disk.vue
@@ -46,7 +46,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.disk
+root(isDark)
> .pie
padding 10px
height 100px
@@ -60,7 +60,7 @@ export default Vue.extend({
> p
margin 0
font-size 12px
- color #505050
+ color isDark ? #a8b4bd : #505050
&:first-child
font-weight bold
@@ -73,4 +73,10 @@ export default Vue.extend({
display block
clear both
+.disk[data-darkmode]
+ root(true)
+
+.disk:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/common/views/widgets/server.memory.vue b/src/client/app/common/views/widgets/server.memory.vue
index 834a62671d..9212f2271f 100644
--- a/src/client/app/common/views/widgets/server.memory.vue
+++ b/src/client/app/common/views/widgets/server.memory.vue
@@ -46,7 +46,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.memory
+root(isDark)
> .pie
padding 10px
height 100px
@@ -60,7 +60,7 @@ export default Vue.extend({
> p
margin 0
font-size 12px
- color #505050
+ color isDark ? #a8b4bd : #505050
&:first-child
font-weight bold
@@ -73,4 +73,10 @@ export default Vue.extend({
display block
clear both
+.memory[data-darkmode]
+ root(true)
+
+.memory:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/common/views/widgets/server.pie.vue b/src/client/app/common/views/widgets/server.pie.vue
index ce2cff1d00..d557c52ea5 100644
--- a/src/client/app/common/views/widgets/server.pie.vue
+++ b/src/client/app/common/views/widgets/server.pie.vue
@@ -45,7 +45,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-svg
+root(isDark)
display block
height 100%
@@ -56,6 +56,12 @@ svg
> text
font-size 0.15px
- fill rgba(0, 0, 0, 0.6)
+ fill isDark ? rgba(#fff, 0.6) : rgba(#000, 0.6)
+
+svg[data-darkmode]
+ root(true)
+
+svg:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/common/views/widgets/server.vue b/src/client/app/common/views/widgets/server.vue
index 2fbc07adf0..2fdd60499b 100644
--- a/src/client/app/common/views/widgets/server.vue
+++ b/src/client/app/common/views/widgets/server.vue
@@ -68,6 +68,7 @@ export default define({
} else {
this.props.view++;
}
+ this.save();
},
func() {
if (this.props.design == 2) {
@@ -75,6 +76,7 @@ export default define({
} else {
this.props.design++;
}
+ this.save();
}
}
});
diff --git a/src/client/app/common/views/widgets/slideshow.vue b/src/client/app/common/views/widgets/slideshow.vue
index ad32299f37..459b24a32f 100644
--- a/src/client/app/common/views/widgets/slideshow.vue
+++ b/src/client/app/common/views/widgets/slideshow.vue
@@ -64,6 +64,7 @@ export default define({
} else {
this.props.size++;
}
+ this.save();
this.applySize();
},
@@ -111,6 +112,7 @@ export default define({
choose() {
(this as any).apis.chooseDriveFolder().then(folder => {
this.props.folder = folder ? folder.id : null;
+ this.save();
this.fetch();
});
}
@@ -122,13 +124,13 @@ export default define({
.mkw-slideshow
overflow hidden
background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
&[data-mobile]
border none
border-radius 8px
- box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+ box-shadow 0 0 0 1px rgba(#000, 0.2)
> div
width 100%
diff --git a/src/client/app/common/views/widgets/tips.vue b/src/client/app/common/views/widgets/tips.vue
index bdecc068e1..08e665f92f 100644
--- a/src/client/app/common/views/widgets/tips.vue
+++ b/src/client/app/common/views/widgets/tips.vue
@@ -17,7 +17,7 @@ const tips = [
'ドライブでファイルをドラッグしてフォルダ移動できます',
'ドライブでフォルダをドラッグしてフォルダ移動できます',
'ホームは設定からカスタマイズできます',
- 'MisskeyはMIT Licenseです',
+ 'MisskeyはAGPLv3です',
'タイムマシンウィジェットを利用すると、簡単に過去のタイムラインに遡れます',
'投稿の ... をクリックして、投稿をユーザーページにピン留めできます',
'ドライブの容量は(デフォルトで)1GBです',
diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts
index dc89adeb86..8ddaebc072 100644
--- a/src/client/app/desktop/api/update-avatar.ts
+++ b/src/client/app/desktop/api/update-avatar.ts
@@ -1,4 +1,4 @@
-import OS from '../../common/mios';
+import OS from '../../mios';
import { apiUrl } from '../../config';
import CropWindow from '../views/components/crop-window.vue';
import ProgressDialog from '../views/components/progress-dialog.vue';
diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts
index bc3f783e35..1a5da272bd 100644
--- a/src/client/app/desktop/api/update-banner.ts
+++ b/src/client/app/desktop/api/update-banner.ts
@@ -1,4 +1,4 @@
-import OS from '../../common/mios';
+import OS from '../../mios';
import { apiUrl } from '../../config';
import CropWindow from '../views/components/crop-window.vue';
import ProgressDialog from '../views/components/progress-dialog.vue';
@@ -95,7 +95,7 @@ export default (os: OS) => {
multiple: false,
title: '%fa:image%バナーにする画像を選択'
});
-
+
return selectedFile
.then(cropImage)
.then(setBanner)
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
index b3152e708b..2658a86b95 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -2,6 +2,7 @@
* Desktop Client
*/
+import Vue from 'vue';
import VueRouter from 'vue-router';
// Style
@@ -24,8 +25,10 @@ import updateBanner from './api/update-banner';
import MkIndex from './views/pages/index.vue';
import MkUser from './views/pages/user/user.vue';
+import MkFavorites from './views/pages/favorites.vue';
import MkSelectDrive from './views/pages/selectdrive.vue';
import MkDrive from './views/pages/drive.vue';
+import MkUserList from './views/pages/user-list.vue';
import MkHomeCustomize from './views/pages/home-customize.vue';
import MkMessagingRoom from './views/pages/messaging-room.vue';
import MkNote from './views/pages/note.vue';
@@ -49,9 +52,11 @@ init(async (launch) => {
routes: [
{ path: '/', name: 'index', component: MkIndex },
{ path: '/i/customize-home', component: MkHomeCustomize },
+ { path: '/i/favorites', component: MkFavorites },
{ path: '/i/messaging/:user', component: MkMessagingRoom },
{ path: '/i/drive', component: MkDrive },
{ path: '/i/drive/folder/:folder', component: MkDrive },
+ { path: '/i/lists/:list', component: MkUserList },
{ path: '/selectdrive', component: MkSelectDrive },
{ path: '/search', component: MkSearch },
{ path: '/othello', component: MkOthello },
diff --git a/src/client/app/desktop/style.styl b/src/client/app/desktop/style.styl
index 49f71fbde7..ea48fbee3d 100644
--- a/src/client/app/desktop/style.styl
+++ b/src/client/app/desktop/style.styl
@@ -44,6 +44,26 @@ html
height 100%
background #f7f7f7
+ &[data-darkmode]
+ background #191B22
+
+ &, *
+ &::-webkit-scrollbar-track
+ background-color #282C37
+
+ &::-webkit-scrollbar
+ width 6px
+ height 6px
+
+ &::-webkit-scrollbar-thumb
+ background-color #454954
+
+ &:hover
+ background-color #535660
+
+ &:active
+ background-color $theme-color
+
body
display flex
flex-direction column
diff --git a/src/client/app/desktop/ui.styl b/src/client/app/desktop/ui.styl
index 5a8d1718e2..b66c8f4025 100644
--- a/src/client/app/desktop/ui.styl
+++ b/src/client/app/desktop/ui.styl
@@ -123,3 +123,59 @@ textarea.ui
font-size 90%
font-weight bold
color rgba(#373a3c, 0.9)
+
+html[data-darkmode]
+ button.ui
+ .button.ui
+ color #fff
+ background linear-gradient(to bottom, #313543 0%, #282c37 100%)
+ border-color #1c2023
+
+ &:hover
+ background linear-gradient(to bottom, #2c2f3c 0%, #22262f 100%)
+ border-color #151a1d
+
+ &:active
+ background #22262f
+ border-color #151a1d
+
+ &.primary
+ color $theme-color-foreground
+ background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+ border solid 1px lighten($theme-color, 15%)
+
+ &:hover:not(:disabled)
+ background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
+ border-color $theme-color
+
+ &:active:not(:disabled)
+ background $theme-color
+ border-color $theme-color
+
+ input:not([type]).ui
+ input[type='text'].ui
+ input[type='password'].ui
+ input[type='email'].ui
+ input[type='date'].ui
+ input[type='number'].ui
+ textarea.ui
+ display block
+ padding 10px
+ width 100%
+ height 40px
+ font-family sans-serif
+ font-size 16px
+ color #dee4e8
+ background #191b22
+ border solid 1px #495156
+ border-radius 4px
+
+ &:hover
+ border-color #b0b0b0
+
+ &:focus
+ border-color $theme-color
+
+ .ui.from.group
+ > p:first-child
+ color #c0c7cc
diff --git a/src/client/app/desktop/views/components/activity.calendar.vue b/src/client/app/desktop/views/components/activity.calendar.vue
index 8b43536c2b..e488571070 100644
--- a/src/client/app/desktop/views/components/activity.calendar.vue
+++ b/src/client/app/desktop/views/components/activity.calendar.vue
@@ -61,6 +61,6 @@ svg
&.day
&:hover
- fill rgba(0, 0, 0, 0.05)
+ fill rgba(#000, 0.05)
</style>
diff --git a/src/client/app/desktop/views/components/activity.vue b/src/client/app/desktop/views/components/activity.vue
index ea33bf9ff6..bd952c39d2 100644
--- a/src/client/app/desktop/views/components/activity.vue
+++ b/src/client/app/desktop/views/components/activity.vue
@@ -1,14 +1,15 @@
<template>
-<div class="mk-activity" :data-melt="design == 2">
- <template v-if="design == 0">
- <p class="title">%fa:chart-bar%%i18n:@title%</p>
- <button @click="toggle" title="%i18n:@toggle%">%fa:sort%</button>
- </template>
- <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
- <template v-else>
- <x-calendar v-show="view == 0" :data="[].concat(activity)"/>
- <x-chart v-show="view == 1" :data="[].concat(activity)"/>
- </template>
+<div class="mk-activity">
+ <mk-widget-container :show-header="design == 0" :naked="design == 2">
+ <template slot="header">%fa:chart-bar%%i18n:@title%</template>
+ <button slot="func" title="%i18n:@toggle%" @click="toggle">%fa:sort%</button>
+
+ <p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+ <template v-else>
+ <x-calendar v-show="view == 0" :data="[].concat(activity)"/>
+ <x-chart v-show="view == 1" :data="[].concat(activity)"/>
+ </template>
+ </mk-widget-container>
</div>
</template>
@@ -64,53 +65,14 @@ export default Vue.extend({
});
</script>
-<style lang="stylus" scoped>
-.mk-activity
- background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
- border-radius 6px
-
- &[data-melt]
- background transparent !important
- border none !important
-
- > .title
- z-index 1
- margin 0
- padding 0 16px
- line-height 42px
- font-size 0.9em
- font-weight bold
- color #888
- box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
- > [data-fa]
- margin-right 4px
-
- > button
- position absolute
- z-index 2
- top 0
- right 0
- padding 0
- width 42px
- font-size 0.9em
- line-height 42px
- color #ccc
-
- &:hover
- color #aaa
-
- &:active
- color #999
-
- > .fetching
- margin 0
- padding 16px
- text-align center
- color #aaa
+<style lang="stylus" module>
+.fetching
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
- > [data-fa]
- margin-right 4px
+ > [data-fa]
+ margin-right 4px
</style>
diff --git a/src/client/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue
index a99b48d195..1d8cc4f3a9 100644
--- a/src/client/app/desktop/views/components/calendar.vue
+++ b/src/client/app/desktop/views/components/calendar.vue
@@ -133,10 +133,10 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-calendar
- color #777
- background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
+root(isDark)
+ color isDark ? #c5ced6 : #777
+ background isDark ? #282C37 : #fff
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
&[data-melt]
@@ -152,7 +152,7 @@ export default Vue.extend({
font-size 0.9em
font-weight bold
color #888
- box-shadow 0 1px rgba(0, 0, 0, 0.07)
+ box-shadow 0 1px rgba(#000, 0.07)
> [data-fa]
margin-right 4px
@@ -214,10 +214,10 @@ export default Vue.extend({
border-radius 6px
&:hover > div
- background rgba(0, 0, 0, 0.025)
+ background rgba(#000, 0.025)
&:active > div
- background rgba(0, 0, 0, 0.05)
+ background rgba(#000, 0.05)
&[data-is-donichi]
color #ef95a0
@@ -233,10 +233,10 @@ export default Vue.extend({
font-weight bold
> div
- background rgba(0, 0, 0, 0.025)
+ background rgba(#000, 0.025)
&:active > div
- background rgba(0, 0, 0, 0.05)
+ background rgba(#000, 0.05)
&[data-today]
> div
@@ -249,4 +249,10 @@ export default Vue.extend({
&:active > div
background darken($theme-color, 10%)
+.mk-calendar[data-darkmode]
+ root(true)
+
+.mk-calendar:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/components/context-menu.menu.vue b/src/client/app/desktop/views/components/context-menu.menu.vue
index 6359dbf1b4..843604a059 100644
--- a/src/client/app/desktop/views/components/context-menu.menu.vue
+++ b/src/client/app/desktop/views/components/context-menu.menu.vue
@@ -31,7 +31,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.menu
+root(isDark)
$width = 240px
$item-height = 38px
$padding = 10px
@@ -46,7 +46,7 @@ export default Vue.extend({
&.divider
margin-top $padding
padding-top $padding
- border-top solid 1px #eee
+ border-top solid 1px isDark ? #1c2023 : #eee
&.nest
> p
@@ -75,7 +75,7 @@ export default Vue.extend({
margin 0
padding 0 32px 0 38px
line-height $item-height
- color #868C8C
+ color isDark ? #c8cece : #868C8C
text-decoration none
cursor pointer
@@ -104,11 +104,17 @@ export default Vue.extend({
left $width
margin-top -($padding)
width $width
- background #fff
+ background isDark ? #282c37 :#fff
border-radius 0 4px 4px 4px
- box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2)
+ box-shadow 2px 2px 8px rgba(#000, 0.2)
transition visibility 0s linear 0.2s
+.menu[data-darkmode]
+ root(true)
+
+.menu:not([data-darkmode])
+ root(false)
+
</style>
<style lang="stylus" module>
diff --git a/src/client/app/desktop/views/components/context-menu.vue b/src/client/app/desktop/views/components/context-menu.vue
index 8bd9945840..60a33f9c93 100644
--- a/src/client/app/desktop/views/components/context-menu.vue
+++ b/src/client/app/desktop/views/components/context-menu.vue
@@ -54,7 +54,7 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.context-menu
+root(isDark)
$width = 240px
$item-height = 38px
$padding = 10px
@@ -66,9 +66,15 @@ export default Vue.extend({
z-index 4096
width $width
font-size 0.8em
- background #fff
+ background isDark ? #282c37 : #fff
border-radius 0 4px 4px 4px
- box-shadow 2px 2px 8px rgba(0, 0, 0, 0.2)
+ box-shadow 2px 2px 8px rgba(#000, 0.2)
opacity 0
+.context-menu[data-darkmode]
+ root(true)
+
+.context-menu:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/components/dialog.vue b/src/client/app/desktop/views/components/dialog.vue
index fa17e4a9d2..aff21c1754 100644
--- a/src/client/app/desktop/views/components/dialog.vue
+++ b/src/client/app/desktop/views/components/dialog.vue
@@ -102,7 +102,7 @@ export default Vue.extend({
left 0
width 100%
height 100%
- background rgba(0, 0, 0, 0.7)
+ background rgba(#000, 0.7)
opacity 0
pointer-events none
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
index d79cb6c09c..39881711fa 100644
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ b/src/client/app/desktop/views/components/drive.file.vue
@@ -186,7 +186,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.root.file
+root(isDark)
padding 8px 0 0 0
height 180px
border-radius 4px
@@ -195,7 +195,7 @@ export default Vue.extend({
cursor pointer
&:hover
- background rgba(0, 0, 0, 0.05)
+ background rgba(#000, 0.05)
> .label
&:before
@@ -203,7 +203,7 @@ export default Vue.extend({
background #0b65a5
&:active
- background rgba(0, 0, 0, 0.1)
+ background rgba(#000, 0.1)
> .label
&:before
@@ -308,10 +308,16 @@ export default Vue.extend({
font-size 0.8em
text-align center
word-break break-all
- color #444
+ color isDark ? #fff : #444
overflow hidden
> .ext
opacity 0.5
+.root.file[data-darkmode]
+ root(true)
+
+.root.file:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue
index 5e91048d19..973df1014d 100644
--- a/src/client/app/desktop/views/components/drive.vue
+++ b/src/client/app/desktop/views/components/drive.vue
@@ -577,7 +577,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-drive
+root(isDark)
> nav
display block
@@ -585,10 +585,9 @@ export default Vue.extend({
width 100%
overflow auto
font-size 0.9em
- color #555
- background #fff
- //border-bottom 1px solid #dfdfdf
- box-shadow 0 1px 0 rgba(0, 0, 0, 0.05)
+ color isDark ? #d2d9dc : #555
+ background isDark ? #282c37 : #fff
+ box-shadow 0 1px 0 rgba(#000, 0.05)
&, *
user-select none
@@ -665,6 +664,7 @@ export default Vue.extend({
padding 8px
height calc(100% - 38px)
overflow auto
+ background isDark ? #191b22 : #fff
&, *
user-select none
@@ -733,7 +733,7 @@ export default Vue.extend({
display inline-block
position absolute
top 0
- background-color rgba(0, 0, 0, 0.3)
+ background-color rgba(#000, 0.3)
border-radius 100%
animation sk-bounce 2.0s infinite ease-in-out
@@ -770,4 +770,10 @@ export default Vue.extend({
> input
display none
+.mk-drive[data-darkmode]
+ root(true)
+
+.mk-drive:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/components/ellipsis-icon.vue b/src/client/app/desktop/views/components/ellipsis-icon.vue
index c54a7db29d..4a5a0f23dc 100644
--- a/src/client/app/desktop/views/components/ellipsis-icon.vue
+++ b/src/client/app/desktop/views/components/ellipsis-icon.vue
@@ -14,7 +14,7 @@
display inline-block
width 18px
height 18px
- background-color rgba(0, 0, 0, 0.3)
+ background-color rgba(#000, 0.3)
border-radius 100%
animation bounce 1.4s infinite ease-in-out both
diff --git a/src/client/app/desktop/views/components/follow-button.vue b/src/client/app/desktop/views/components/follow-button.vue
index 9eb22b0fb8..60c6129f61 100644
--- a/src/client/app/desktop/views/components/follow-button.vue
+++ b/src/client/app/desktop/views/components/follow-button.vue
@@ -19,6 +19,7 @@
<script lang="ts">
import Vue from 'vue';
+
export default Vue.extend({
props: {
user: {
@@ -30,6 +31,7 @@ export default Vue.extend({
default: 'compact'
}
},
+
data() {
return {
wait: false,
@@ -37,6 +39,7 @@ export default Vue.extend({
connectionId: null
};
},
+
mounted() {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
@@ -44,13 +47,14 @@ export default Vue.extend({
this.connection.on('follow', this.onFollow);
this.connection.on('unfollow', this.onUnfollow);
},
+
beforeDestroy() {
this.connection.off('follow', this.onFollow);
this.connection.off('unfollow', this.onUnfollow);
(this as any).os.stream.dispose(this.connectionId);
},
- methods: {
+ methods: {
onFollow(user) {
if (user.id == this.user.id) {
this.user.isFollowing = user.isFollowing;
@@ -94,7 +98,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-follow-button
+root(isDark)
display block
cursor pointer
padding 0
@@ -121,17 +125,17 @@ export default Vue.extend({
border-radius 8px
&.follow
- color #888
- background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
- border solid 1px #e2e2e2
+ color isDark ? #fff : #888
+ background isDark ? linear-gradient(to bottom, #313543 0%, #282c37 100%) : linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
+ border solid 1px isDark ? #1c2023 : #e2e2e2
&:hover
- background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
- border-color #dcdcdc
+ background isDark ? linear-gradient(to bottom, #2c2f3c 0%, #22262f 100%) : linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
+ border-color isDark ? #151a1d : #dcdcdc
&:active
- background #ececec
- border-color #dcdcdc
+ background isDark ? #22262f : #ececec
+ border-color isDark ? #151a1d : #dcdcdc
&.unfollow
color $theme-color-foreground
@@ -161,4 +165,10 @@ export default Vue.extend({
i
margin-right 8px
+.mk-follow-button[data-darkmode]
+ root(true)
+
+.mk-follow-button:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index af5bde3ad5..3c1f8b8257 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -3,9 +3,7 @@
<p class="title">気になるユーザーをフォロー:</p>
<div class="users" v-if="!fetching && users.length > 0">
<div class="user" v-for="user in users" :key="user.id">
- <router-link class="avatar-anchor" :to="user | userPage">
- <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="user.id"/>
- </router-link>
+ <mk-avatar class="avatar" :user="user" target="_blank"/>
<div class="body">
<router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link>
<p class="username">@{{ user | acct }}</p>
@@ -86,18 +84,13 @@ export default Vue.extend({
display block
clear both
- > .avatar-anchor
+ > .avatar
display block
float left
margin 0 12px 0 0
-
- > .avatar
- display block
- width 42px
- height 42px
- margin 0
- border-radius 8px
- vertical-align bottom
+ width 42px
+ height 42px
+ border-radius 8px
> .body
float left
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index 90e9d1b785..cae6233cd8 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -53,7 +53,7 @@
<div class="main">
<a @click="hint">カスタマイズのヒント</a>
<div>
- <mk-post-form v-if="os.i.clientSettings.showPostFormOnTopOfTl"/>
+ <mk-post-form v-if="clientSettings.showPostFormOnTopOfTl"/>
<mk-timeline ref="tl" @loaded="onTlLoaded"/>
</div>
</div>
@@ -63,7 +63,7 @@
<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp"/>
</div>
<div class="main">
- <mk-post-form v-if="os.i.clientSettings.showPostFormOnTopOfTl"/>
+ <mk-post-form v-if="clientSettings.showPostFormOnTopOfTl"/>
<mk-timeline ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>
<mk-mentions @loaded="onTlLoaded" v-if="mode == 'mentions'"/>
</div>
@@ -81,6 +81,7 @@ export default Vue.extend({
components: {
XDraggable
},
+
props: {
customize: {
type: Boolean,
@@ -91,61 +92,43 @@ export default Vue.extend({
default: 'timeline'
}
},
+
data() {
return {
connection: null,
connectionId: null,
widgetAdderSelected: null,
- trash: [],
- widgets: {
- left: [],
- right: []
- }
+ trash: []
};
},
+
computed: {
- home: {
- get(): any[] {
- //#region 互換性のため
- (this as any).os.i.clientSettings.home.forEach(w => {
- if (w.name == 'rss-reader') w.name = 'rss';
- if (w.name == 'user-recommendation') w.name = 'users';
- if (w.name == 'recommended-polls') w.name = 'polls';
- });
- //#endregion
- return (this as any).os.i.clientSettings.home;
- },
- set(value) {
- (this as any).os.i.clientSettings.home = value;
- }
+ home(): any[] {
+ return this.$store.state.settings.data.home;
},
left(): any[] {
return this.home.filter(w => w.place == 'left');
},
right(): any[] {
return this.home.filter(w => w.place == 'right');
+ },
+ widgets(): any {
+ return {
+ left: this.left,
+ right: this.right
+ };
}
},
- created() {
- this.widgets.left = this.left;
- this.widgets.right = this.right;
- this.$watch('os.i.clientSettings', i => {
- this.widgets.left = this.left;
- this.widgets.right = this.right;
- }, {
- deep: true
- });
- },
+
mounted() {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
-
- this.connection.on('home_updated', this.onHomeUpdated);
},
+
beforeDestroy() {
- this.connection.off('home_updated', this.onHomeUpdated);
(this as any).os.stream.dispose(this.connectionId);
},
+
methods: {
hint() {
(this as any).apis.dialog({
@@ -159,56 +142,44 @@ export default Vue.extend({
}]
});
},
+
onTlLoaded() {
this.$emit('loaded');
},
- onHomeUpdated(data) {
- if (data.home) {
- (this as any).os.i.clientSettings.home = data.home;
- this.widgets.left = data.home.filter(w => w.place == 'left');
- this.widgets.right = data.home.filter(w => w.place == 'right');
- } else {
- const w = (this as any).os.i.clientSettings.home.find(w => w.id == data.id);
- if (w != null) {
- w.data = data.data;
- this.$refs[w.id][0].preventSave = true;
- this.$refs[w.id][0].props = w.data;
- this.widgets.left = (this as any).os.i.clientSettings.home.filter(w => w.place == 'left');
- this.widgets.right = (this as any).os.i.clientSettings.home.filter(w => w.place == 'right');
- }
- }
- },
+
onWidgetContextmenu(widgetId) {
const w = (this.$refs[widgetId] as any)[0];
if (w.func) w.func();
},
+
onWidgetSort() {
this.saveHome();
},
+
onTrash(evt) {
this.saveHome();
},
+
addWidget() {
- const widget = {
+ this.$store.dispatch('settings/addHomeWidget', {
name: this.widgetAdderSelected,
id: uuid(),
place: 'left',
data: {}
- };
-
- this.widgets.left.unshift(widget);
- this.saveHome();
+ });
},
+
saveHome() {
const left = this.widgets.left;
const right = this.widgets.right;
- this.home = left.concat(right);
+ this.$store.commit('settings/setHome', left.concat(right));
left.forEach(w => w.place = 'left');
right.forEach(w => w.place = 'right');
(this as any).api('i/update_home', {
home: this.home
});
},
+
warp(date) {
(this.$refs.tl as any).warp(date);
}
@@ -219,7 +190,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-home
+root(isDark)
display block
&[data-customize]
@@ -249,8 +220,9 @@ export default Vue.extend({
left 0
width 100%
height 48px
- background #f7f7f7
- box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
+ color isDark ? #fff : #000
+ background isDark ? #313543 : #f7f7f7
+ box-shadow 0 1px 1px rgba(#000, 0.075)
> a
display block
@@ -278,7 +250,7 @@ export default Vue.extend({
> div
display flex
margin 0 auto
- max-width 1200px - 32px
+ max-width 1220px - 32px
> div
width 50%
@@ -289,7 +261,7 @@ export default Vue.extend({
line-height 48px
&.trash
- border-left solid 1px #ddd
+ border-left solid 1px isDark ? #1c2023 : #ddd
> div
width 100%
@@ -309,7 +281,7 @@ export default Vue.extend({
display flex
justify-content center
margin 0 auto
- max-width 1200px
+ max-width 1220px
> *
.customize-container
@@ -329,7 +301,7 @@ export default Vue.extend({
.mk-post-form
margin-bottom 16px
- border solid 1px #e5e5e5
+ border solid 1px rgba(#000, 0.075)
border-radius 4px
> *:not(.main)
@@ -357,4 +329,10 @@ export default Vue.extend({
max-width 700px
margin 0 auto
+.mk-home[data-darkmode]
+ root(true)
+
+.mk-home:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts
index 4f61f43692..f58d0706df 100644
--- a/src/client/app/desktop/views/components/index.ts
+++ b/src/client/app/desktop/views/components/index.ts
@@ -28,6 +28,7 @@ import friendsMaker from './friends-maker.vue';
import followers from './followers.vue';
import following from './following.vue';
import usersList from './users-list.vue';
+import userListTimeline from './user-list-timeline.vue';
import widgetContainer from './widget-container.vue';
Vue.component('mk-ui', ui);
@@ -58,4 +59,5 @@ Vue.component('mk-friends-maker', friendsMaker);
Vue.component('mk-followers', followers);
Vue.component('mk-following', following);
Vue.component('mk-users-list', usersList);
+Vue.component('mk-user-list-timeline', userListTimeline);
Vue.component('mk-widget-container', widgetContainer);
diff --git a/src/client/app/desktop/views/components/media-image-dialog.vue b/src/client/app/desktop/views/components/media-image-dialog.vue
index dec140d1c9..026522d907 100644
--- a/src/client/app/desktop/views/components/media-image-dialog.vue
+++ b/src/client/app/desktop/views/components/media-image-dialog.vue
@@ -52,7 +52,7 @@ export default Vue.extend({
left 0
width 100%
height 100%
- background rgba(0, 0, 0, 0.7)
+ background rgba(#000, 0.7)
> img
position fixed
diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue
index 51309a0578..e5803cc36e 100644
--- a/src/client/app/desktop/views/components/media-image.vue
+++ b/src/client/app/desktop/views/components/media-image.vue
@@ -14,12 +14,20 @@ import Vue from 'vue';
import MkMediaImageDialog from './media-image-dialog.vue';
export default Vue.extend({
- props: ['image'],
+ props: {
+ image: {
+ type: Object,
+ required: true
+ },
+ raw: {
+ default: false
+ }
+ },
computed: {
style(): any {
return {
'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
- 'background-image': `url(${this.image.url}?thumbnail&size=512)`
+ 'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
};
}
},
@@ -31,7 +39,7 @@ export default Vue.extend({
const xp = mouseX / this.$el.offsetWidth * 100;
const yp = mouseY / this.$el.offsetHeight * 100;
this.$el.style.backgroundPosition = xp + '% ' + yp + '%';
- this.$el.style.backgroundImage = 'url("' + this.image.url + '?thumbnail")';
+ this.$el.style.backgroundImage = `url("${this.image.url}")`;
},
onMouseleave() {
diff --git a/src/client/app/desktop/views/components/media-video-dialog.vue b/src/client/app/desktop/views/components/media-video-dialog.vue
index cbf862cd1c..959cefa42c 100644
--- a/src/client/app/desktop/views/components/media-video-dialog.vue
+++ b/src/client/app/desktop/views/components/media-video-dialog.vue
@@ -54,7 +54,7 @@ export default Vue.extend({
left 0
width 100%
height 100%
- background rgba(0, 0, 0, 0.7)
+ background rgba(#000, 0.7)
> video
position fixed
diff --git a/src/client/app/desktop/views/components/media-video.vue b/src/client/app/desktop/views/components/media-video.vue
index 4fd955a821..3635941e64 100644
--- a/src/client/app/desktop/views/components/media-video.vue
+++ b/src/client/app/desktop/views/components/media-video.vue
@@ -52,6 +52,7 @@ export default Vue.extend({
width 100%
height 100%
border-radius 4px
+
.mk-media-video-thumbnail
display flex
justify-content center
diff --git a/src/client/app/desktop/views/components/mentions.vue b/src/client/app/desktop/views/components/mentions.vue
index fc3a7af75d..66bdab5c08 100644
--- a/src/client/app/desktop/views/components/mentions.vue
+++ b/src/client/app/desktop/views/components/mentions.vue
@@ -1,8 +1,8 @@
<template>
<div class="mk-mentions">
<header>
- <span :data-is-active="mode == 'all'" @click="mode = 'all'">すべて</span>
- <span :data-is-active="mode == 'following'" @click="mode = 'following'">フォロー中</span>
+ <span :data-active="mode == 'all'" @click="mode = 'all'">すべて</span>
+ <span :data-active="mode == 'following'" @click="mode = 'following'">フォロー中</span>
</header>
<div class="fetching" v-if="fetching">
<mk-ellipsis-icon/>
@@ -85,7 +85,7 @@ export default Vue.extend({
.mk-mentions
background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
> header
@@ -98,7 +98,7 @@ export default Vue.extend({
font-size 18px
color #555
- &:not([data-is-active])
+ &:not([data-active])
color $theme-color
cursor pointer
diff --git a/src/client/app/desktop/views/components/note-detail.sub.vue b/src/client/app/desktop/views/components/note-detail.sub.vue
index 16bc2a1d98..24550c4e94 100644
--- a/src/client/app/desktop/views/components/note-detail.sub.vue
+++ b/src/client/app/desktop/views/components/note-detail.sub.vue
@@ -1,8 +1,6 @@
<template>
<div class="sub" :title="title">
- <router-link class="avatar-anchor" :to="note.user | userPage">
- <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
- </router-link>
+ <mk-avatar class="avatar" :user="note.user"/>
<div class="main">
<header>
<div class="left">
@@ -16,8 +14,11 @@
</div>
</header>
<div class="body">
- <mk-note-html v-if="note.text" :text="note.text" :i="os.i" :class="$style.text"/>
- <div class="media" v-if="note.media > 0">
+ <div class="text">
+ <span v-if="note.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
+ <mk-note-html v-if="note.text" :text="note.text" :i="os.i"/>
+ </div>
+ <div class="media" v-if="note.mediaIds.length > 0">
<mk-media-list :media-list="note.media"/>
</div>
</div>
@@ -40,10 +41,10 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.sub
+root(isDark)
margin 0
padding 20px 32px
- background #fdfdfd
+ background isDark ? #21242d : #fdfdfd
&:after
content ""
@@ -54,18 +55,13 @@ export default Vue.extend({
> .main > footer > button
color #888
- > .avatar-anchor
+ > .avatar
display block
float left
margin 0 16px 0 0
-
- > .avatar
- display block
- width 44px
- height 44px
- margin 0
- border-radius 4px
- vertical-align bottom
+ width 44px
+ height 44px
+ border-radius 4px
> .main
float left
@@ -87,7 +83,7 @@ export default Vue.extend({
display inline
margin 0
padding 0
- color #777
+ color isDark ? #fff : #777
font-size 1em
font-weight 700
text-align left
@@ -99,24 +95,29 @@ export default Vue.extend({
> .username
text-align left
margin 0 0 0 8px
- color #ccc
+ color isDark ? #606984 : #ccc
> .right
float right
> .time
font-size 0.9em
- color #c0c0c0
+ color isDark ? #606984 : #c0c0c0
-</style>
+ > .body
+ > .text
+ cursor default
+ display block
+ margin 0
+ padding 0
+ overflow-wrap break-word
+ font-size 1em
+ color isDark ? #959ba7 : #717171
+
+.sub[data-darkmode]
+ root(true)
+
+.sub:not([data-darkmode])
+ root(false)
-<style lang="stylus" module>
-.text
- cursor default
- display block
- margin 0
- padding 0
- overflow-wrap break-word
- font-size 1em
- color #717171
</style>
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index b62a7cfd61..a0e3915149 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -18,18 +18,14 @@
</div>
<div class="renote" v-if="isRenote">
<p>
- <router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
- <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
- </router-link>
+ <mk-avatar class="avatar" :user="note.user"/>
%fa:retweet%
<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
がRenote
</p>
</div>
<article>
- <router-link class="avatar-anchor" :to="p.user | userPage">
- <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
- </router-link>
+ <mk-avatar class="avatar" :user="p.user"/>
<header>
<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
<span class="username">@{{ p.user | acct }}</span>
@@ -38,9 +34,12 @@
</router-link>
</header>
<div class="body">
- <mk-note-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/>
+ <div class="text">
+ <span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
+ <mk-note-html v-if="p.text" :text="p.text" :i="os.i"/>
+ </div>
<div class="media" v-if="p.media.length > 0">
- <mk-media-list :media-list="p.media"/>
+ <mk-media-list :media-list="p.media" :raw="true"/>
</div>
<mk-poll v-if="p.poll" :note="p"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
@@ -56,7 +55,9 @@
<footer>
<mk-reactions-viewer :note="p"/>
<button @click="reply" title="返信">
- %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+ <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">
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
@@ -154,7 +155,7 @@ export default Vue.extend({
// Draw map
if (this.p.geo) {
- const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+ const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
if (shouldShowMap) {
(this as any).os.getGoogleMaps().then(maps => {
const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
@@ -212,13 +213,13 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-note-detail
- margin 0
+root(isDark)
+ margin 0 auto
padding 0
overflow hidden
text-align left
- background #fff
- border solid 1px rgba(0, 0, 0, 0.1)
+ background isDark ? #282C37 : #fff
+ border solid 1px rgba(#000, 0.1)
border-radius 8px
> .read-more
@@ -230,44 +231,39 @@ export default Vue.extend({
text-align center
color #999
cursor pointer
- background #fafafa
+ background isDark ? #21242d : #fafafa
outline none
border none
- border-bottom solid 1px #eef0f2
+ border-bottom solid 1px isDark ? #1c2023 : #eef0f2
border-radius 6px 6px 0 0
&:hover
- background #f6f6f6
+ background isDark ? #2e3440 : #f6f6f6
&:active
- background #f0f0f0
+ background isDark ? #21242b : #f0f0f0
&:disabled
- color #ccc
+ color isDark ? #21242b : #ccc
> .context
> *
- border-bottom 1px solid #eef0f2
+ border-bottom 1px solid isDark ? #1c2023 : #eef0f2
> .renote
color #9dbb00
- background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+ background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
> p
margin 0
padding 16px 32px
- .avatar-anchor
+ .avatar
display inline-block
-
- .avatar
- vertical-align bottom
- min-width 28px
- min-height 28px
- max-width 28px
- max-height 28px
- margin 0 8px 0 0
- border-radius 6px
+ width 28px
+ height 28px
+ margin 0 8px 0 0
+ border-radius 6px
[data-fa]
margin-right 4px
@@ -279,7 +275,7 @@ export default Vue.extend({
padding-top 8px
> .reply-to
- border-bottom 1px solid #eef0f2
+ border-bottom 1px solid isDark ? #1c2023 : #eef0f2
> article
padding 28px 32px 18px 32px
@@ -290,21 +286,13 @@ export default Vue.extend({
clear both
&:hover
- > .main > footer > button
- color #888
+ > footer > button
+ color isDark ? #707b97 : #888
- > .avatar-anchor
- display block
+ > .avatar
width 60px
height 60px
-
- > .avatar
- display block
- width 60px
- height 60px
- margin 0
- border-radius 8px
- vertical-align bottom
+ border-radius 8px
> header
position absolute
@@ -316,7 +304,7 @@ export default Vue.extend({
display inline-block
margin 0
line-height 24px
- color #777
+ color isDark ? #fff : #627079
font-size 18px
font-weight 700
text-align left
@@ -329,18 +317,27 @@ export default Vue.extend({
display block
text-align left
margin 0
- color #ccc
+ color isDark ? #606984 : #ccc
> .time
position absolute
top 0
right 32px
font-size 1em
- color #c0c0c0
+ color isDark ? #606984 : #c0c0c0
> .body
padding 8px 0
+ > .text
+ cursor default
+ display block
+ margin 0
+ padding 0
+ overflow-wrap break-word
+ font-size 1.5em
+ color isDark ? #fff : #717171
+
> .renote
margin 8px 0
@@ -402,11 +399,11 @@ export default Vue.extend({
background transparent
border none
font-size 1em
- color #ddd
+ color isDark ? #606984 : #ccc
cursor pointer
&:hover
- color #666
+ color isDark ? #9198af : #666
> .count
display inline
@@ -418,17 +415,12 @@ export default Vue.extend({
> .replies
> *
- border-top 1px solid #eef0f2
+ border-top 1px solid isDark ? #1c2023 : #eef0f2
-</style>
+.mk-note-detail[data-darkmode]
+ root(true)
+
+.mk-note-detail:not([data-darkmode])
+ root(false)
-<style lang="stylus" module>
-.text
- cursor default
- display block
- margin 0
- padding 0
- overflow-wrap break-word
- font-size 1.5em
- color #717171
</style>
diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue
index ff3ecadc20..d04abfc5a7 100644
--- a/src/client/app/desktop/views/components/note-preview.vue
+++ b/src/client/app/desktop/views/components/note-preview.vue
@@ -1,8 +1,6 @@
<template>
<div class="mk-note-preview" :title="title">
- <router-link class="avatar-anchor" :to="note.user | userPage">
- <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
- </router-link>
+ <mk-avatar class="avatar" :user="note.user"/>
<div class="main">
<header>
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
@@ -33,31 +31,21 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-note-preview
+root(isDark)
font-size 0.9em
- background #fff
&:after
content ""
display block
clear both
- &:hover
- > .main > footer > button
- color #888
-
- > .avatar-anchor
+ > .avatar
display block
float left
margin 0 16px 0 0
-
- > .avatar
- display block
- width 52px
- height 52px
- margin 0
- border-radius 8px
- vertical-align bottom
+ width 52px
+ height 52px
+ border-radius 8px
> .main
float left
@@ -65,12 +53,13 @@ export default Vue.extend({
> header
display flex
+ align-items baseline
white-space nowrap
> .name
margin 0 .5em 0 0
padding 0
- color #607073
+ color isDark ? #fff : #607073
font-size 1em
font-weight bold
text-decoration none
@@ -81,11 +70,11 @@ export default Vue.extend({
> .username
margin 0 .5em 0 0
- color #d1d8da
+ color isDark ? #606984 : #d1d8da
> .time
margin-left auto
- color #b2b8bb
+ color isDark ? #606984 : #b2b8bb
> .body
@@ -94,6 +83,12 @@ export default Vue.extend({
margin 0
padding 0
font-size 1.1em
- color #717171
+ color isDark ? #959ba7 : #717171
+
+.mk-note-preview[data-darkmode]
+ root(true)
+
+.mk-note-preview:not([data-darkmode])
+ root(false)
</style>
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 e854785783..575d605203 100644
--- a/src/client/app/desktop/views/components/notes.note.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -1,15 +1,22 @@
<template>
<div class="sub" :title="title">
- <router-link class="avatar-anchor" :to="note.user | userPage">
- <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
- </router-link>
+ <mk-avatar class="avatar" :user="note.user"/>
<div class="main">
<header>
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
<span class="username">@{{ note.user | acct }}</span>
- <router-link class="created-at" :to="note | notePage">
- <mk-time :time="note.createdAt"/>
- </router-link>
+ <div class="info">
+ <span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
+ <router-link class="created-at" :to="note | notePage">
+ <mk-time :time="note.createdAt"/>
+ </router-link>
+ <span class="visibility" v-if="note.visibility != 'public'">
+ <template v-if="note.visibility == 'home'">%fa:home%</template>
+ <template v-if="note.visibility == 'followers'">%fa:unlock%</template>
+ <template v-if="note.visibility == 'specified'">%fa:envelope%</template>
+ <template v-if="note.visibility == 'private'">%fa:lock%</template>
+ </span>
+ </div>
</header>
<div class="body">
<mk-sub-note-content class="text" :note="note"/>
@@ -33,32 +40,24 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.sub
+root(isDark)
margin 0
- padding 16px
+ padding 16px 32px
font-size 0.9em
+ background isDark ? #21242d : #fcfcfc
&:after
content ""
display block
clear both
- &:hover
- > .main > footer > button
- color #888
-
- > .avatar-anchor
+ > .avatar
display block
float left
margin 0 14px 0 0
-
- > .avatar
- display block
- width 52px
- height 52px
- margin 0
- border-radius 8px
- vertical-align bottom
+ width 52px
+ height 52px
+ border-radius 8px
> .main
float left
@@ -66,6 +65,7 @@ export default Vue.extend({
> header
display flex
+ align-items baseline
margin-bottom 2px
white-space nowrap
line-height 21px
@@ -75,7 +75,7 @@ export default Vue.extend({
margin 0 .5em 0 0
padding 0
overflow hidden
- color #607073
+ color isDark ? #fff : #607073
font-size 1em
font-weight bold
text-decoration none
@@ -86,23 +86,40 @@ export default Vue.extend({
> .username
margin 0 .5em 0 0
- color #d1d8da
+ color isDark ? #606984 : #d1d8da
- > .created-at
+ > .info
margin-left auto
- color #b2b8bb
+ font-size 0.9em
+
+ > *
+ color isDark ? #606984 : #b2b8bb
+
+ > .mobile
+ margin-right 6px
+
+ > .visibility
+ margin-left 6px
> .body
+ max-height 128px
+ overflow hidden
> .text
cursor default
margin 0
padding 0
font-size 1.1em
- color #717171
+ color isDark ? #959ba7 : #717171
pre
max-height 120px
font-size 80%
+.sub[data-darkmode]
+ root(true)
+
+.sub:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 326ec4dc89..057c3c0956 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -1,24 +1,18 @@
<template>
<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
- <div class="reply-to" v-if="p.reply">
+ <div class="reply-to" v-if="p.reply && (!os.isSignedIn || clientSettings.showReplyTarget)">
<x-sub :note="p.reply"/>
</div>
<div class="renote" v-if="isRenote">
- <p>
- <router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
- <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
- </router-link>
- %fa:retweet%
- <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>
- </p>
+ <mk-avatar class="avatar" :user="note.user"/>
+ %fa:retweet%
+ <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"/>
</div>
<article>
- <router-link class="avatar-anchor" :to="p.user | userPage">
- <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
- </router-link>
+ <mk-avatar class="avatar" :user="p.user"/>
<div class="main">
<header>
<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
@@ -30,35 +24,50 @@
<router-link class="created-at" :to="p | notePage">
<mk-time :time="p.createdAt"/>
</router-link>
+ <span class="visibility" v-if="p.visibility != 'public'">
+ <template v-if="p.visibility == 'home'">%fa:home%</template>
+ <template v-if="p.visibility == 'followers'">%fa:unlock%</template>
+ <template v-if="p.visibility == 'specified'">%fa:envelope%</template>
+ <template v-if="p.visibility == 'private'">%fa:lock%</template>
+ </span>
</div>
</header>
<div class="body">
<p class="channel" v-if="p.channel">
<a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:
</p>
- <div class="text">
- <a class="reply" v-if="p.reply">%fa:reply%</a>
- <mk-note-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/>
- <a class="rp" v-if="p.renote">RP:</a>
- </div>
- <div class="media" v-if="p.media.length > 0">
- <mk-media-list :media-list="p.media"/>
- </div>
- <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
- <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>
- <div class="map" v-if="p.geo" ref="map"></div>
- <div class="renote" v-if="p.renote">
- <mk-note-preview :note="p.renote"/>
+ <p v-if="p.cw != null" class="cw">
+ <span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
+ <span class="toggle" @click="showContent = !showContent">{{ showContent ? '隠す' : 'もっと見る' }}</span>
+ </p>
+ <div class="content" v-show="p.cw == null || showContent">
+ <div class="text">
+ <span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
+ <a class="reply" v-if="p.reply">%fa:reply%</a>
+ <mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
+ <a class="rp" v-if="p.renote">RP:</a>
+ </div>
+ <div class="media" v-if="p.media.length > 0">
+ <mk-media-list :media-list="p.media"/>
+ </div>
+ <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
+ <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>
+ <div class="map" v-if="p.geo" ref="map"></div>
+ <div class="renote" v-if="p.renote">
+ <mk-note-preview :note="p.renote"/>
+ </div>
+ <mk-url-preview v-for="url in urls" :url="url" :key="url"/>
</div>
- <mk-url-preview v-for="url in urls" :url="url" :key="url"/>
</div>
<footer>
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
<button @click="reply" title="%i18n:@reply%">
- %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+ <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="%i18n:@renote%">
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
@@ -113,6 +122,7 @@ export default Vue.extend({
data() {
return {
+ showContent: false,
isDetailOpened: false,
connection: null,
connectionId: null
@@ -168,7 +178,7 @@ export default Vue.extend({
// Draw map
if (this.p.geo) {
- const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+ const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
if (shouldShowMap) {
(this as any).os.getGoogleMaps().then(maps => {
const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
@@ -289,20 +299,21 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.note
+root(isDark)
margin 0
padding 0
- background #fff
- border-bottom solid 1px #eaeaea
-
- &:first-child
- border-top-left-radius 6px
- border-top-right-radius 6px
+ background isDark ? #282C37 : #fff
+ border-bottom solid 1px isDark ? #1c2023 : #eaeaea
- > .renote
+ &[data-round]
+ &:first-child
border-top-left-radius 6px
border-top-right-radius 6px
+ > .renote
+ border-top-left-radius 6px
+ border-top-right-radius 6px
+
&:last-of-type
border-bottom none
@@ -321,47 +332,45 @@ export default Vue.extend({
border-radius 4px
> .renote
+ display flex
+ align-items center
+ padding 16px 32px
+ line-height 28px
color #9dbb00
- background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+ background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
- > p
- margin 0
- padding 16px 32px
- line-height 28px
+ .avatar
+ display inline-block
+ width 28px
+ height 28px
+ margin 0 8px 0 0
+ border-radius 6px
- .avatar-anchor
- display inline-block
+ [data-fa]
+ margin-right 4px
- .avatar
- vertical-align bottom
- width 28px
- height 28px
- margin 0 8px 0 0
- border-radius 6px
+ > span
+ flex-shrink 0
- [data-fa]
- margin-right 4px
+ &:last-of-type
+ margin-right 8px
- .name
- font-weight bold
+ .name
+ overflow hidden
+ flex-shrink 1
+ text-overflow ellipsis
+ white-space nowrap
+ font-weight bold
> .mk-time
- position absolute
- top 16px
- right 32px
+ display block
+ margin-left auto
+ flex-shrink 0
font-size 0.9em
- line-height 28px
& + article
padding-top 8px
- > .reply-to
- padding 0 16px
- background rgba(0, 0, 0, 0.0125)
-
- > .mk-note-preview
- background transparent
-
> article
padding 28px 32px 18px 32px
@@ -372,31 +381,26 @@ export default Vue.extend({
&:hover
> .main > footer > button
- color #888
+ color isDark ? #707b97 : #888
- > .avatar-anchor
+ > .avatar
display block
float left
margin 0 16px 10px 0
+ width 58px
+ height 58px
+ border-radius 8px
//position -webkit-sticky
//position sticky
//top 74px
- > .avatar
- display block
- width 58px
- height 58px
- margin 0
- border-radius 8px
- vertical-align bottom
-
> .main
float left
width calc(100% - 74px)
> header
display flex
- align-items center
+ align-items baseline
margin-bottom 4px
white-space nowrap
@@ -405,7 +409,7 @@ export default Vue.extend({
margin 0 .5em 0 0
padding 0
overflow hidden
- color #627079
+ color isDark ? #fff : #627079
font-size 1em
font-weight bold
text-decoration none
@@ -418,114 +422,156 @@ export default Vue.extend({
margin 0 .5em 0 0
padding 1px 6px
font-size 12px
- color #aaa
- border solid 1px #ddd
+ color isDark ? #758188 : #aaa
+ border solid 1px isDark ? #57616f : #ddd
border-radius 3px
> .username
margin 0 .5em 0 0
- color #ccc
+ overflow hidden
+ text-overflow ellipsis
+ color isDark ? #606984 : #ccc
> .info
margin-left auto
font-size 0.9em
+ > *
+ color isDark ? #606984 : #c0c0c0
+
> .mobile
margin-right 8px
- color #ccc
> .app
margin-right 8px
padding-right 8px
- color #ccc
border-right solid 1px #eaeaea
- > .created-at
- color #c0c0c0
+ > .visibility
+ margin-left 8px
> .body
- > .text
+ > .cw
cursor default
display block
margin 0
padding 0
overflow-wrap break-word
font-size 1.1em
- color #717171
-
- >>> .quote
- margin 8px
- padding 6px 12px
- color #aaa
- border-left solid 3px #eee
+ color isDark ? #fff : #717171
- > .reply
+ > .text
margin-right 8px
- color #717171
- > .rp
- margin-left 4px
- font-style oblique
- color #a0bf46
+ > .toggle
+ display inline-block
+ padding 4px 8px
+ font-size 0.7em
+ color isDark ? #393f4f : #fff
+ background isDark ? #687390 : #b1b9c1
+ border-radius 2px
+ cursor pointer
+ user-select none
- > .location
- margin 4px 0
- font-size 12px
- color #ccc
+ &:hover
+ background isDark ? #707b97 : #bbc4ce
- > .map
- width 100%
- height 300px
+ > .content
- &:empty
- display none
+ > .text
+ cursor default
+ display block
+ margin 0
+ padding 0
+ overflow-wrap break-word
+ font-size 1.1em
+ color isDark ? #fff : #717171
- > .tags
- margin 4px 0 0 0
+ >>> .title
+ display block
+ margin-bottom 4px
+ padding 4px
+ font-size 90%
+ text-align center
+ background isDark ? #2f3944 : #eef1f3
+ border-radius 4px
- > *
- display inline-block
- margin 0 8px 0 0
- padding 2px 8px 2px 16px
- font-size 90%
- color #8d969e
- background #edf0f3
- border-radius 4px
+ >>> .code
+ margin 8px 0
- &:before
- content ""
- display block
- position absolute
- top 0
- bottom 0
- left 4px
- width 8px
- height 8px
- margin auto 0
- background #fff
- border-radius 100%
+ >>> .quote
+ margin 8px
+ padding 6px 12px
+ color isDark ? #6f808e : #aaa
+ border-left solid 3px isDark ? #637182 : #eee
- &:hover
- text-decoration none
- background #e2e7ec
+ > .reply
+ margin-right 8px
+ color isDark ? #99abbf : #717171
- .mk-url-preview
- margin-top 8px
+ > .rp
+ margin-left 4px
+ font-style oblique
+ color #a0bf46
- > .channel
- margin 0
+ > .location
+ margin 4px 0
+ font-size 12px
+ color #ccc
+
+ > .map
+ width 100%
+ height 300px
- > .mk-poll
- font-size 80%
+ &:empty
+ display none
- > .renote
- margin 8px 0
+ > .tags
+ margin 4px 0 0 0
- > .mk-note-preview
- padding 16px
- border dashed 1px #c0dac6
- border-radius 8px
+ > *
+ display inline-block
+ margin 0 8px 0 0
+ padding 2px 8px 2px 16px
+ font-size 90%
+ color #8d969e
+ background #edf0f3
+ border-radius 4px
+
+ &:before
+ content ""
+ display block
+ position absolute
+ top 0
+ bottom 0
+ left 4px
+ width 8px
+ height 8px
+ margin auto 0
+ background #fff
+ border-radius 100%
+
+ &:hover
+ text-decoration none
+ background #e2e7ec
+
+ .mk-url-preview
+ margin-top 8px
+
+ > .channel
+ margin 0
+
+ > .mk-poll
+ font-size 80%
+
+ > .renote
+ margin 8px 0
+
+ > .mk-note-preview
+ padding 16px
+ border dashed 1px isDark ? #4e945e : #c0dac6
+ border-radius 8px
> footer
> button
@@ -533,13 +579,13 @@ export default Vue.extend({
padding 0 8px
line-height 32px
font-size 1em
- color #ddd
+ color isDark ? #606984 : #ddd
background transparent
border none
cursor pointer
&:hover
- color #666
+ color isDark ? #9198af : #666
> .count
display inline
@@ -556,7 +602,13 @@ export default Vue.extend({
> .detail
padding-top 4px
- background rgba(0, 0, 0, 0.0125)
+ background rgba(#000, 0.0125)
+
+.note[data-darkmode]
+ root(true)
+
+.note:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index b5f6957a16..7e80e6f74a 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -1,32 +1,65 @@
<template>
<div class="mk-notes">
- <template v-for="(note, i) in _notes">
- <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
- <p class="date" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
- <span>%fa:angle-up%{{ note._datetext }}</span>
- <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
- </p>
- </template>
- <footer>
- <slot name="footer"></slot>
+ <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
+
+ <slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
+
+ <div v-if="!fetching && requestInitPromise != null">
+ <p>読み込みに失敗しました。</p>
+ <button @click="resolveInitPromise">リトライ</button>
+ </div>
+
+ <transition-group name="mk-notes" class="transition">
+ <template v-for="(note, i) in _notes">
+ <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
+ <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
+ <span>%fa:angle-up%{{ note._datetext }}</span>
+ <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
+ </p>
+ </template>
+ </transition-group>
+
+ <footer v-if="more">
+ <button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+ <template v-if="!moreFetching">%i18n:@load-more%</template>
+ <template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
+ </button>
</footer>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
+import { url } from '../../../config';
+import getNoteSummary from '../../../../../renderers/get-note-summary';
+
import XNote from './notes.note.vue';
+const displayLimit = 30;
+
export default Vue.extend({
components: {
XNote
},
+
props: {
- notes: {
- type: Array,
- default: () => []
+ more: {
+ type: Function,
+ required: false
}
},
+
+ data() {
+ return {
+ requestInitPromise: null as () => Promise<any[]>,
+ notes: [],
+ queue: [],
+ unreadCount: 0,
+ fetching: true,
+ moreFetching: false
+ };
+ },
+
computed: {
_notes(): any[] {
return (this.notes as any).map(note => {
@@ -38,52 +71,202 @@ export default Vue.extend({
});
}
},
+
+ mounted() {
+ document.addEventListener('visibilitychange', this.onVisibilitychange, false);
+ window.addEventListener('scroll', this.onScroll);
+ },
+
+ beforeDestroy() {
+ document.removeEventListener('visibilitychange', this.onVisibilitychange);
+ window.removeEventListener('scroll', this.onScroll);
+ },
+
methods: {
+ isScrollTop() {
+ return window.scrollY <= 8;
+ },
+
focus() {
(this.$el as any).children[0].focus();
},
+
onNoteUpdated(i, note) {
Vue.set((this as any).notes, i, note);
+ },
+
+ init(promiseGenerator: () => Promise<any[]>) {
+ this.requestInitPromise = promiseGenerator;
+ this.resolveInitPromise();
+ },
+
+ resolveInitPromise() {
+ this.queue = [];
+ this.notes = [];
+ this.fetching = true;
+
+ const promise = this.requestInitPromise();
+
+ promise.then(notes => {
+ this.notes = notes;
+ this.requestInitPromise = null;
+ this.fetching = false;
+ }, e => {
+ this.fetching = false;
+ });
+ },
+
+ prepend(note, silent = false) {
+ //#region 弾く
+ const isMyNote = note.userId == (this as any).os.i.id;
+ const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
+
+ if ((this as any).clientSettings.showMyRenotes === false) {
+ if (isMyNote && isPureRenote) {
+ return;
+ }
+ }
+
+ if ((this as any).clientSettings.showRenotedMyNotes === false) {
+ if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) {
+ return;
+ }
+ }
+ //#endregion
+
+ // 投稿が自分のものではないかつ、タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
+ if ((document.hidden || !this.isScrollTop()) && note.userId !== (this as any).os.i.id) {
+ this.unreadCount++;
+ document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
+ }
+
+ if (this.isScrollTop()) {
+ // Prepend the note
+ this.notes.unshift(note);
+
+ // サウンドを再生する
+ if ((this as any).os.isEnableSounds && !silent) {
+ const sound = new Audio(`${url}/assets/post.mp3`);
+ sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+ sound.play();
+ }
+
+ // オーバーフローしたら古い投稿は捨てる
+ if (this.notes.length >= displayLimit) {
+ this.notes = this.notes.slice(0, displayLimit);
+ }
+ } else {
+ this.queue.push(note);
+ }
+ },
+
+ append(note) {
+ this.notes.push(note);
+ },
+
+ tail() {
+ return this.notes[this.notes.length - 1];
+ },
+
+ releaseQueue() {
+ this.queue.forEach(n => this.prepend(n, true));
+ this.queue = [];
+ },
+
+ async loadMore() {
+ if (this.more == null) return;
+ if (this.moreFetching) return;
+
+ this.moreFetching = true;
+ await this.more();
+ this.moreFetching = false;
+ },
+
+ clearNotification() {
+ this.unreadCount = 0;
+ document.title = 'Misskey';
+ },
+
+ onVisibilitychange() {
+ if (!document.hidden) {
+ this.clearNotification();
+ }
+ },
+
+ onScroll() {
+ if (this.isScrollTop()) {
+ this.releaseQueue();
+ this.clearNotification();
+ }
+
+ if ((this as any).clientSettings.fetchOnScroll !== false) {
+ const current = window.scrollY + window.innerHeight;
+ if (current > document.body.offsetHeight - 8) this.loadMore();
+ }
}
}
});
</script>
<style lang="stylus" scoped>
-.mk-notes
+@import '~const.styl'
+
+root(isDark)
+ .transition
+ .mk-notes-enter
+ .mk-notes-leave-to
+ opacity 0
+ transform translateY(-30px)
+
+ > *
+ transition transform .3s ease, opacity .3s ease
- > .date
- display block
- margin 0
- line-height 32px
- font-size 14px
- text-align center
- color #aaa
- background #fdfdfd
- border-bottom solid 1px #eaeaea
+ > .date
+ display block
+ margin 0
+ line-height 32px
+ font-size 14px
+ text-align center
+ color isDark ? #666b79 : #aaa
+ background isDark ? #242731 : #fdfdfd
+ border-bottom solid 1px isDark ? #1c2023 : #eaeaea
- span
- margin 0 16px
+ span
+ margin 0 16px
- [data-fa]
- margin-right 8px
+ [data-fa]
+ margin-right 8px
+
+ > .newer-indicator
+ position -webkit-sticky
+ position sticky
+ z-index 100
+ height 3px
+ background $theme-color
> footer
- > *
+ > button
display block
margin 0
padding 16px
width 100%
text-align center
color #ccc
- border-top solid 1px #eaeaea
- border-bottom-left-radius 4px
- border-bottom-right-radius 4px
+ background isDark ? #282C37 : #fff
+ border-top solid 1px isDark ? #1c2023 : #eaeaea
+ border-bottom-left-radius 6px
+ border-bottom-right-radius 6px
- > button
&:hover
- background #f5f5f5
+ background isDark ? #2e3440 : #f5f5f5
&:active
- background #eee
+ background isDark ? #21242b : #eee
+
+.mk-notes[data-darkmode]
+ root(true)
+
+.mk-notes:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 413a87755a..7923d1a62d 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -1,96 +1,84 @@
<template>
<div class="mk-notifications">
<div class="notifications" v-if="notifications.length != 0">
- <template v-for="(notification, i) in _notifications">
- <div class="notification" :class="notification.type" :key="notification.id">
- <mk-time :time="notification.createdAt"/>
- <template v-if="notification.type == 'reaction'">
- <router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
- <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
- </router-link>
- <div class="text">
- <p>
- <mk-reaction-icon :reaction="notification.reaction"/>
- <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
- </p>
- <router-link class="note-ref" :to="notification.note | notePage">
- %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
- </router-link>
- </div>
- </template>
- <template v-if="notification.type == 'renote'">
- <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
- <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
- </router-link>
- <div class="text">
- <p>%fa:retweet%
- <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
- </p>
- <router-link class="note-ref" :to="notification.note | notePage">
- %fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
- </router-link>
- </div>
- </template>
- <template v-if="notification.type == 'quote'">
- <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
- <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
- </router-link>
- <div class="text">
- <p>%fa:quote-left%
- <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
- </p>
- <router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
- </div>
- </template>
- <template v-if="notification.type == 'follow'">
- <router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
- <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
- </router-link>
- <div class="text">
- <p>%fa:user-plus%
- <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
- </p>
- </div>
- </template>
- <template v-if="notification.type == 'reply'">
- <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
- <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
- </router-link>
- <div class="text">
- <p>%fa:reply%
- <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
- </p>
- <router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
- </div>
- </template>
- <template v-if="notification.type == 'mention'">
- <router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
- <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
- </router-link>
- <div class="text">
- <p>%fa:at%
- <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
- </p>
- <a class="note-preview" :href="notification.note | notePage">{{ getNoteSummary(notification.note) }}</a>
- </div>
- </template>
- <template v-if="notification.type == 'poll_vote'">
- <router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
- <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
- </router-link>
- <div class="text">
- <p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p>
- <router-link class="note-ref" :to="notification.note | notePage">
- %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
- </router-link>
- </div>
- </template>
- </div>
- <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
- <span>%fa:angle-up%{{ notification._datetext }}</span>
- <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
- </p>
- </template>
+ <transition-group name="mk-notifications" class="transition">
+ <template v-for="(notification, i) in _notifications">
+ <div class="notification" :class="notification.type" :key="notification.id">
+ <mk-time :time="notification.createdAt"/>
+ <template v-if="notification.type == 'reaction'">
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div class="text">
+ <p>
+ <mk-reaction-icon :reaction="notification.reaction"/>
+ <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
+ </p>
+ <router-link class="note-ref" :to="notification.note | notePage">
+ %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
+ </router-link>
+ </div>
+ </template>
+ <template v-if="notification.type == 'renote'">
+ <mk-avatar class="avatar" :user="notification.note.user"/>
+ <div class="text">
+ <p>%fa:retweet%
+ <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
+ </p>
+ <router-link class="note-ref" :to="notification.note | notePage">
+ %fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
+ </router-link>
+ </div>
+ </template>
+ <template v-if="notification.type == 'quote'">
+ <mk-avatar class="avatar" :user="notification.note.user"/>
+ <div class="text">
+ <p>%fa:quote-left%
+ <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
+ </p>
+ <router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
+ </div>
+ </template>
+ <template v-if="notification.type == 'follow'">
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div class="text">
+ <p>%fa:user-plus%
+ <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
+ </p>
+ </div>
+ </template>
+ <template v-if="notification.type == 'reply'">
+ <mk-avatar class="avatar" :user="notification.note.user"/>
+ <div class="text">
+ <p>%fa:reply%
+ <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
+ </p>
+ <router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
+ </div>
+ </template>
+ <template v-if="notification.type == 'mention'">
+ <mk-avatar class="avatar" :user="notification.note.user"/>
+ <div class="text">
+ <p>%fa:at%
+ <router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
+ </p>
+ <a class="note-preview" :href="notification.note | notePage">{{ getNoteSummary(notification.note) }}</a>
+ </div>
+ </template>
+ <template v-if="notification.type == 'poll_vote'">
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div class="text">
+ <p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p>
+ <router-link class="note-ref" :to="notification.note | notePage">
+ %fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
+ </router-link>
+ </div>
+ </template>
+ </div>
+ <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
+ <span>%fa:angle-up%{{ notification._datetext }}</span>
+ <span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
+ </p>
+ </template>
+ </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%' }}
@@ -185,111 +173,116 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-notifications
- > .notifications
- > .notification
- margin 0
- padding 16px
- overflow-wrap break-word
- font-size 0.9em
- border-bottom solid 1px rgba(0, 0, 0, 0.05)
+root(isDark)
+ .transition
+ .mk-notifications-enter
+ .mk-notifications-leave-to
+ opacity 0
+ transform translateY(-30px)
- &:last-child
- border-bottom none
+ > *
+ transition transform .3s ease, opacity .3s ease
- > .mk-time
- display inline
- position absolute
- top 16px
- right 12px
- vertical-align top
- color rgba(0, 0, 0, 0.6)
- font-size small
+ > .notifications
+ > *
+ > .notification
+ margin 0
+ padding 16px
+ overflow-wrap break-word
+ font-size 0.9em
+ border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05)
- &:after
- content ""
- display block
- clear both
+ &:last-child
+ border-bottom none
- > .avatar-anchor
- display block
- float left
- position -webkit-sticky
- position sticky
- top 16px
+ > .mk-time
+ display inline
+ position absolute
+ top 16px
+ right 12px
+ vertical-align top
+ color isDark ? #606984 : rgba(#000, 0.6)
+ font-size small
+
+ &:after
+ content ""
+ display block
+ clear both
- > img
+ > .avatar
display block
- min-width 36px
- min-height 36px
- max-width 36px
- max-height 36px
+ float left
+ position -webkit-sticky
+ position sticky
+ top 16px
+ width 36px
+ height 36px
border-radius 6px
- > .text
- float right
- width calc(100% - 36px)
- padding-left 8px
+ > .text
+ float right
+ width calc(100% - 36px)
+ padding-left 8px
- p
- margin 0
+ p
+ margin 0
- i, .mk-reaction-icon
- margin-right 4px
+ i, .mk-reaction-icon
+ margin-right 4px
- .note-preview
- color rgba(0, 0, 0, 0.7)
+ .note-preview
+ color isDark ? #c2cad4 : rgba(#000, 0.7)
- .note-ref
- color rgba(0, 0, 0, 0.7)
+ .note-ref
+ color isDark ? #c2cad4 : rgba(#000, 0.7)
- [data-fa]
- font-size 1em
- font-weight normal
- font-style normal
- display inline-block
- margin-right 3px
+ [data-fa]
+ font-size 1em
+ font-weight normal
+ font-style normal
+ display inline-block
+ margin-right 3px
- &.renote, &.quote
- .text p i
- color #77B255
+ &.renote, &.quote
+ .text p i
+ color #77B255
- &.follow
- .text p i
- color #53c7ce
+ &.follow
+ .text p i
+ color #53c7ce
- &.reply, &.mention
- .text p i
- color #555
+ &.reply, &.mention
+ .text p i
+ color #555
- > .date
- display block
- margin 0
- line-height 32px
- text-align center
- font-size 0.8em
- color #aaa
- background #fdfdfd
- border-bottom solid 1px rgba(0, 0, 0, 0.05)
+ > .date
+ display block
+ margin 0
+ line-height 32px
+ text-align center
+ font-size 0.8em
+ color isDark ? #666b79 : #aaa
+ background isDark ? #242731 : #fdfdfd
+ border-bottom solid 1px isDark ? #1c2023 : rgba(#000, 0.05)
- span
- margin 0 16px
+ span
+ margin 0 16px
- [data-fa]
- margin-right 8px
+ [data-fa]
+ margin-right 8px
> .more
display block
width 100%
padding 16px
color #555
- border-top solid 1px rgba(0, 0, 0, 0.05)
+ border-top solid 1px rgba(#000, 0.05)
&:hover
- background rgba(0, 0, 0, 0.025)
+ background rgba(#000, 0.025)
&:active
- background rgba(0, 0, 0, 0.05)
+ background rgba(#000, 0.05)
&.fetching
cursor wait
@@ -312,4 +305,10 @@ export default Vue.extend({
> [data-fa]
margin-right 4px
+.mk-notifications[data-darkmode]
+ root(true)
+
+.mk-notifications:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index ebb0193088..984fc9866c 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -6,6 +6,11 @@
@drop.stop="onDrop"
>
<div class="content">
+ <div v-if="visibility == 'specified'" class="visibleUsers">
+ <span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
+ <a @click="addVisibleUser">+ユーザーを追加</a>
+ </div>
+ <input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)">
<textarea :class="{ with: (files.length != 0 || poll) }"
ref="text" v-model="text" :disabled="posting"
@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
@@ -27,8 +32,10 @@
<button class="drive" title="%i18n:@attach-media-from-drive%" @click="chooseFileFromDrive">%fa:cloud%</button>
<button class="kao" title="%i18n:@insert-a-kao%" @click="kao">%fa:R smile%</button>
<button class="poll" title="%i18n:@create-poll%" @click="poll = true">%fa:chart-pie%</button>
+ <button class="poll" title="内容を隠す" @click="useCw = !useCw">%fa:eye-slash%</button>
<button class="geo" title="位置情報を添付する" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
- <p class="text-count" :class="{ over: text.length > 1000 }">{{ '%i18n:!@text-remain%'.replace('{}', 1000 - text.length) }}</p>
+ <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"/>
</button>
@@ -41,12 +48,16 @@
import Vue from 'vue';
import * as XDraggable from 'vuedraggable';
import getKao from '../../../common/scripts/get-kao';
+import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
export default Vue.extend({
components: {
- XDraggable
+ XDraggable,
+ MkVisibilityChooser
},
+
props: ['reply', 'renote'],
+
data() {
return {
posting: false,
@@ -54,11 +65,16 @@ export default Vue.extend({
files: [],
uploadings: [],
poll: false,
+ useCw: false,
+ cw: null,
geo: null,
+ visibility: 'public',
+ visibleUsers: [],
autocomplete: null,
draghover: false
};
},
+
computed: {
draftId(): string {
return this.renote
@@ -67,6 +83,7 @@ export default Vue.extend({
? 'reply:' + this.reply.id
: 'note';
},
+
placeholder(): string {
return this.renote
? '%i18n:!@quote-placeholder%'
@@ -74,6 +91,7 @@ export default Vue.extend({
? '%i18n:!@reply-placeholder%'
: '%i18n:!@note-placeholder%';
},
+
submitText(): string {
return this.renote
? '%i18n:!@renote%'
@@ -81,22 +99,17 @@ export default Vue.extend({
? '%i18n:!@reply%'
: '%i18n:!@note%';
},
+
canPost(): boolean {
return !this.posting && (this.text.length != 0 || this.files.length != 0 || this.poll || this.renote);
}
},
- watch: {
- text() {
- this.saveDraft();
- },
- poll() {
- this.saveDraft();
- },
- files() {
- this.saveDraft();
- }
- },
+
mounted() {
+ if (this.reply && this.reply.user.host != null) {
+ this.text = `@${this.reply.user.username}@${this.reply.user.host} `;
+ }
+
this.$nextTick(() => {
// 書きかけの投稿を復元
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
@@ -111,15 +124,26 @@ export default Vue.extend({
}
this.$emit('change-attached-media', this.files);
}
+
+ this.$nextTick(() => this.watch());
});
},
+
methods: {
+ watch() {
+ this.$watch('text', () => this.saveDraft());
+ this.$watch('poll', () => this.saveDraft());
+ this.$watch('files', () => this.saveDraft());
+ },
+
focus() {
(this.$refs.text as any).focus();
},
+
chooseFile() {
(this.$refs.file as any).click();
},
+
chooseFileFromDrive() {
(this as any).apis.chooseDriveFile({
multiple: true
@@ -127,32 +151,40 @@ export default Vue.extend({
files.forEach(this.attachMedia);
});
},
+
attachMedia(driveFile) {
this.files.push(driveFile);
this.$emit('change-attached-media', this.files);
},
+
detachMedia(id) {
this.files = this.files.filter(x => x.id != id);
this.$emit('change-attached-media', this.files);
},
+
onChangeFile() {
Array.from((this.$refs.file as any).files).forEach(this.upload);
},
+
upload(file) {
(this.$refs.uploader as any).upload(file);
},
+
onChangeUploadings(uploads) {
this.$emit('change-uploadings', uploads);
},
+
clear() {
this.text = '';
this.files = [];
this.poll = false;
this.$emit('change-attached-media', this.files);
},
+
onKeydown(e) {
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
},
+
onPaste(e) {
Array.from(e.clipboardData.items).forEach((item: any) => {
if (item.kind == 'file') {
@@ -160,6 +192,7 @@ export default Vue.extend({
}
});
},
+
onDragover(e) {
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
@@ -169,12 +202,15 @@ export default Vue.extend({
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
}
},
+
onDragenter(e) {
this.draghover = true;
},
+
onDragleave(e) {
this.draghover = false;
},
+
onDrop(e): void {
this.draghover = false;
@@ -195,6 +231,7 @@ export default Vue.extend({
}
//#endregion
},
+
setGeo() {
if (navigator.geolocation == null) {
alert('お使いの端末は位置情報に対応していません');
@@ -210,10 +247,38 @@ export default Vue.extend({
enableHighAccuracy: true
});
},
+
removeGeo() {
this.geo = null;
this.$emit('geo-dettached');
},
+
+ setVisibility() {
+ const w = (this as any).os.new(MkVisibilityChooser, {
+ source: this.$refs.visibilityButton,
+ v: this.visibility
+ });
+ w.$once('chosen', v => {
+ this.visibility = v;
+ });
+ },
+
+ addVisibleUser() {
+ (this as any).apis.input({
+ title: 'ユーザー名を入力してください'
+ }).then(username => {
+ (this as any).api('users/show', {
+ username
+ }).then(user => {
+ this.visibleUsers.push(user);
+ });
+ });
+ },
+
+ removeVisibleUser(user) {
+ this.visibleUsers = this.visibleUsers.filter(u => u != user);
+ },
+
post() {
this.posting = true;
@@ -223,6 +288,9 @@ export default Vue.extend({
replyId: this.reply ? this.reply.id : undefined,
renoteId: this.renote ? this.renote.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
+ cw: this.useCw ? this.cw || '' : undefined,
+ visibility: this.visibility,
+ visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
geo: this.geo ? {
coordinates: [this.geo.longitude, this.geo.latitude],
altitude: this.geo.altitude,
@@ -250,6 +318,7 @@ export default Vue.extend({
this.posting = false;
});
},
+
saveDraft() {
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
@@ -264,6 +333,7 @@ export default Vue.extend({
localStorage.setItem('drafts', JSON.stringify(data));
},
+
deleteDraft() {
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
@@ -271,6 +341,7 @@ export default Vue.extend({
localStorage.setItem('drafts', JSON.stringify(data));
},
+
kao() {
this.text += getKao();
}
@@ -281,10 +352,10 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-post-form
+root(isDark)
display block
padding 16px
- background lighten($theme-color, 95%)
+ background isDark ? #282C37 : lighten($theme-color, 95%)
&:after
content ""
@@ -292,56 +363,70 @@ export default Vue.extend({
clear both
> .content
-
- textarea
+ > input
+ > textarea
display block
- padding 12px
- margin 0
width 100%
- max-width 100%
- min-width 100%
- min-height calc(16px + 12px + 12px)
+ padding 12px
font-size 16px
- color #333
- background #fff
+ color isDark ? #fff : #333
+ background isDark ? #191d23 : #fff
outline none
border solid 1px rgba($theme-color, 0.1)
border-radius 4px
- transition border-color .3s ease
+ transition border-color .2s ease
&:hover
border-color rgba($theme-color, 0.2)
transition border-color .1s ease
+ &:focus
+ border-color rgba($theme-color, 0.5)
+ transition border-color 0s ease
+
+ &:disabled
+ opacity 0.5
+
+ &::-webkit-input-placeholder
+ color rgba($theme-color, 0.3)
+
+ > input
+ margin-bottom 8px
+
+ > textarea
+ margin 0
+ max-width 100%
+ min-width 100%
+ min-height 64px
+
+ &:hover
& + *
& + * + *
border-color rgba($theme-color, 0.2)
transition border-color .1s ease
&:focus
- color $theme-color
- border-color rgba($theme-color, 0.5)
- transition border-color 0s ease
-
& + *
& + * + *
border-color rgba($theme-color, 0.5)
transition border-color 0s ease
- &:disabled
- opacity 0.5
-
- &::-webkit-input-placeholder
- color rgba($theme-color, 0.3)
-
&.with
border-bottom solid 1px rgba($theme-color, 0.1) !important
border-radius 4px 4px 0 0
+ > .visibleUsers
+ margin-bottom 8px
+ font-size 14px
+
+ > span
+ margin-right 16px
+ color isDark ? #fff : #666
+
> .medias
margin 0
padding 0
- background lighten($theme-color, 98%)
+ background isDark ? #181b23 : lighten($theme-color, 98%)
border solid 1px rgba($theme-color, 0.1)
border-top none
border-radius 0 0 4px 4px
@@ -392,7 +477,7 @@ export default Vue.extend({
cursor pointer
> .mk-poll-editor
- background lighten($theme-color, 98%)
+ background isDark ? #181b23 : lighten($theme-color, 98%)
border solid 1px rgba($theme-color, 0.1)
border-top none
border-radius 0 0 4px 4px
@@ -407,19 +492,6 @@ export default Vue.extend({
input[type='file']
display none
- .text-count
- pointer-events none
- display block
- position absolute
- bottom 16px
- right 138px
- margin 0
- line-height 40px
- color rgba($theme-color, 0.5)
-
- &.over
- color #ec3828
-
.submit
display block
position absolute
@@ -484,11 +556,25 @@ export default Vue.extend({
from {background-position: 0 0;}
to {background-position: -64px 32px;}
+ > .text-count
+ pointer-events none
+ display block
+ position absolute
+ bottom 16px
+ right 138px
+ margin 0
+ line-height 40px
+ color rgba($theme-color, 0.5)
+
+ &.over
+ color #ec3828
+
> .upload
> .drive
> .kao
> .poll
> .geo
+ > .visibility
display inline-block
cursor pointer
padding 0
@@ -496,7 +582,7 @@ export default Vue.extend({
width 40px
height 40px
font-size 1em
- color rgba($theme-color, 0.5)
+ color isDark ? $theme-color : rgba($theme-color, 0.5)
background transparent
outline none
border solid 1px transparent
@@ -504,13 +590,13 @@ export default Vue.extend({
&:hover
background transparent
- border-color rgba($theme-color, 0.3)
+ border-color isDark ? rgba($theme-color, 0.5) : rgba($theme-color, 0.3)
&:active
color rgba($theme-color, 0.6)
- background linear-gradient(to bottom, lighten($theme-color, 80%) 0%, lighten($theme-color, 90%) 100%)
+ background isDark ? transparent : linear-gradient(to bottom, lighten($theme-color, 80%) 0%, lighten($theme-color, 90%) 100%)
border-color rgba($theme-color, 0.5)
- box-shadow 0 2px 4px rgba(0, 0, 0, 0.15) inset
+ box-shadow 0 2px 4px rgba(#000, 0.15) inset
&:focus
&:after
@@ -533,4 +619,10 @@ export default Vue.extend({
border dashed 2px rgba($theme-color, 0.5)
pointer-events none
+.mk-post-form[data-darkmode]
+ root(true)
+
+.mk-post-form:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue
index daae5df5e9..9c0154211b 100644
--- a/src/client/app/desktop/views/components/renote-form.vue
+++ b/src/client/app/desktop/views/components/renote-form.vue
@@ -4,8 +4,8 @@
<template v-if="!quote">
<footer>
<a class="quote" v-if="!quote" @click="onQuote">%i18n:@quote%</a>
- <button class="cancel" @click="cancel">%i18n:@cancel%</button>
- <button class="ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:!@reposting%' : '%i18n:!@renote%' }}</button>
+ <button class="ui cancel" @click="cancel">%i18n:@cancel%</button>
+ <button class="ui primary ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:!@reposting%' : '%i18n:!@renote%' }}</button>
</footer>
</template>
<template v-if="quote">
@@ -59,14 +59,14 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-renote-form
+root(isDark)
> .mk-note-preview
margin 16px 22px
> footer
height 72px
- background lighten($theme-color, 95%)
+ background isDark ? #313543 : lighten($theme-color, 95%)
> .quote
position absolute
@@ -78,54 +78,19 @@ export default Vue.extend({
display block
position absolute
bottom 16px
- cursor pointer
- padding 0
- margin 0
width 120px
height 40px
- font-size 1em
- outline none
- border-radius 4px
- &:focus
- &:after
- content ""
- pointer-events none
- position absolute
- top -5px
- right -5px
- bottom -5px
- left -5px
- border 2px solid rgba($theme-color, 0.3)
- border-radius 8px
+ &.cancel
+ right 148px
- > .cancel
- right 148px
- color #888
- background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
- border solid 1px #e2e2e2
+ &.ok
+ right 16px
- &:hover
- background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
- border-color #dcdcdc
+.mk-renote-form[data-darkmode]
+ root(true)
- &:active
- background #ececec
- border-color #dcdcdc
-
- > .ok
- right 16px
- font-weight bold
- color $theme-color-foreground
- background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
- border solid 1px lighten($theme-color, 15%)
-
- &:hover
- background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
- border-color $theme-color
-
- &:active
- background $theme-color
- border-color $theme-color
+.mk-renote-form:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/desktop/views/components/repost-form.vue b/src/client/app/desktop/views/components/repost-form.vue
deleted file mode 100644
index d5b1696757..0000000000
--- a/src/client/app/desktop/views/components/repost-form.vue
+++ /dev/null
@@ -1,131 +0,0 @@
-<template>
-<div class="mk-renote-form">
- <mk-note-preview :note="note"/>
- <template v-if="!quote">
- <footer>
- <a class="quote" v-if="!quote" @click="onQuote">%i18n:desktop.tags.mk-renote-form.quote%</a>
- <button class="cancel" @click="cancel">%i18n:desktop.tags.mk-renote-form.cancel%</button>
- <button class="ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:!desktop.tags.mk-renote-form.reposting%' : '%i18n:!desktop.tags.mk-renote-form.renote%' }}</button>
- </footer>
- </template>
- <template v-if="quote">
- <mk-post-form ref="form" :renote="note" @posted="onChildFormPosted"/>
- </template>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
- props: ['note'],
- data() {
- return {
- wait: false,
- quote: false
- };
- },
- methods: {
- ok() {
- this.wait = true;
- (this as any).api('notes/create', {
- renoteId: this.note.id
- }).then(data => {
- this.$emit('posted');
- (this as any).apis.notify('%i18n:!desktop.tags.mk-renote-form.success%');
- }).catch(err => {
- (this as any).apis.notify('%i18n:!desktop.tags.mk-renote-form.failure%');
- }).then(() => {
- this.wait = false;
- });
- },
- cancel() {
- this.$emit('canceled');
- },
- onQuote() {
- this.quote = true;
-
- this.$nextTick(() => {
- (this.$refs.form as any).focus();
- });
- },
- onChildFormPosted() {
- this.$emit('posted');
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-@import '~const.styl'
-
-.mk-renote-form
-
- > .mk-note-preview
- margin 16px 22px
-
- > footer
- height 72px
- background lighten($theme-color, 95%)
-
- > .quote
- position absolute
- bottom 16px
- left 28px
- line-height 40px
-
- button
- display block
- position absolute
- bottom 16px
- cursor pointer
- padding 0
- margin 0
- width 120px
- height 40px
- font-size 1em
- outline none
- border-radius 4px
-
- &:focus
- &:after
- content ""
- pointer-events none
- position absolute
- top -5px
- right -5px
- bottom -5px
- left -5px
- border 2px solid rgba($theme-color, 0.3)
- border-radius 8px
-
- > .cancel
- right 148px
- color #888
- background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
- border solid 1px #e2e2e2
-
- &:hover
- background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
- border-color #dcdcdc
-
- &:active
- background #ececec
- border-color #dcdcdc
-
- > .ok
- right 16px
- font-weight bold
- color $theme-color-foreground
- background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
- border solid 1px lighten($theme-color, 15%)
-
- &:hover
- background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
- border-color $theme-color
-
- &:active
- background $theme-color
- border-color $theme-color
-
-</style>
diff --git a/src/client/app/desktop/views/components/settings.api.vue b/src/client/app/desktop/views/components/settings.api.vue
index a43c6e8ea6..377f2e689b 100644
--- a/src/client/app/desktop/views/components/settings.api.vue
+++ b/src/client/app/desktop/views/components/settings.api.vue
@@ -29,8 +29,6 @@ export default Vue.extend({
<style lang="stylus" scoped>
.root.api
- color #4a535a
-
code
display inline-block
padding 4px 6px
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 3d88ccb6c2..9439ded2fc 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -20,7 +20,7 @@
<section class="web" v-show="page == 'web'">
<h1>動作</h1>
- <mk-switch v-model="os.i.clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
+ <mk-switch v-model="clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll" text="スクロールで自動読み込み">
<span>ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。</span>
</mk-switch>
<mk-switch v-model="autoPopout" text="ウィンドウの自動ポップアウト">
@@ -37,13 +37,20 @@
<section class="web" v-show="page == 'web'">
<h1>デザインと表示</h1>
<div class="div">
- <button class="ui button" @click="customizeHome">ホームをカスタマイズ</button>
+ <button class="ui button" @click="customizeHome" style="margin-bottom: 16px">ホームをカスタマイズ</button>
</div>
- <mk-switch v-model="os.i.clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
- <mk-switch v-model="os.i.clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
+ <div class="div">
+ <mk-switch v-model="darkmode" text="ダークモード"/>
+ <mk-switch v-model="clientSettings.circleIcons" @change="onChangeCircleIcons" text="円形のアイコンを使用"/>
+ <mk-switch v-model="clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
+ </div>
+ <mk-switch v-model="clientSettings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="タイムライン上部に投稿フォームを表示する"/>
+ <mk-switch v-model="clientSettings.showReplyTarget" @change="onChangeShowReplyTarget" text="リプライ先を表示する"/>
+ <mk-switch v-model="clientSettings.showMyRenotes" @change="onChangeShowMyRenotes" text="自分の行ったRenoteをタイムラインに表示する"/>
+ <mk-switch v-model="clientSettings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="Renoteされた自分の投稿をタイムラインに表示する"/>
+ <mk-switch v-model="clientSettings.showMaps" @change="onChangeShowMaps" text="マップの自動展開">
<span>位置情報が添付された投稿のマップを自動的に展開します。</span>
</mk-switch>
- <mk-switch v-model="os.i.clientSettings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="ウィンドウのタイトルバーにグラデーションを使用"/>
</section>
<section class="web" v-show="page == 'web'">
@@ -63,7 +70,7 @@
<section class="web" v-show="page == 'web'">
<h1>モバイル</h1>
- <mk-switch v-model="os.i.clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
+ <mk-switch v-model="clientSettings.disableViaMobile" @change="onChangeDisableViaMobile" text="「モバイルからの投稿」フラグを付けない"/>
</section>
<section class="web" v-show="page == 'web'">
@@ -76,6 +83,7 @@
<el-option label="ja-JP" value="ja"/>
<el-option label="en-US" value="en"/>
<el-option label="fr" value="fr"/>
+ <el-option label="pl" value="pl"/>
</el-option-group>
</el-select>
<div class="none ui info">
@@ -228,6 +236,7 @@ export default Vue.extend({
version,
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,
@@ -251,6 +260,9 @@ export default Vue.extend({
apiViaStream() {
localStorage.setItem('apiViaStream', this.apiViaStream ? 'true' : 'false');
},
+ darkmode() {
+ (this as any)._updateDarkmode_(this.darkmode);
+ },
enableSounds() {
localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false');
},
@@ -287,8 +299,8 @@ export default Vue.extend({
this.$emit('done');
},
onChangeFetchOnScroll(v) {
- (this as any).api('i/update_client_setting', {
- name: 'fetchOnScroll',
+ this.$store.dispatch('settings/set', {
+ key: 'fetchOnScroll',
value: v
});
},
@@ -297,27 +309,57 @@ export default Vue.extend({
autoWatch: v
});
},
+ onChangeDark(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'dark',
+ value: v
+ });
+ },
onChangeShowPostFormOnTopOfTl(v) {
- (this as any).api('i/update_client_setting', {
- name: 'showPostFormOnTopOfTl',
+ this.$store.dispatch('settings/set', {
+ key: 'showPostFormOnTopOfTl',
+ 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
});
},
onChangeShowMaps(v) {
- (this as any).api('i/update_client_setting', {
- name: 'showMaps',
+ this.$store.dispatch('settings/set', {
+ key: 'showMaps',
+ value: v
+ });
+ },
+ onChangeCircleIcons(v) {
+ this.$store.dispatch('settings/set', {
+ key: 'circleIcons',
value: v
});
},
onChangeGradientWindowHeader(v) {
- (this as any).api('i/update_client_setting', {
- name: 'gradientWindowHeader',
+ this.$store.dispatch('settings/set', {
+ key: 'gradientWindowHeader',
value: v
});
},
onChangeDisableViaMobile(v) {
- (this as any).api('i/update_client_setting', {
- name: 'disableViaMobile',
+ this.$store.dispatch('settings/set', {
+ key: 'disableViaMobile',
value: v
});
},
@@ -358,7 +400,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-settings
+root(isDark)
display flex
width 100%
height 100%
@@ -369,13 +411,13 @@ export default Vue.extend({
height 100%
padding 16px 0 0 0
overflow auto
- border-right solid 1px #ddd
+ border-right solid 1px isDark ? #1c2023 : #ddd
> p
display block
padding 10px 16px
margin 0
- color #666
+ color isDark ? #9aa2a7 : #666
cursor pointer
user-select none
transition margin-left 0.2s ease
@@ -384,7 +426,7 @@ export default Vue.extend({
margin-right 4px
&:hover
- color #555
+ color isDark ? #fff : #555
&.active
margin-left 8px
@@ -398,14 +440,14 @@ export default Vue.extend({
> section
margin 32px
- color #4a535a
+ color isDark ? #c4ccd2 : #4a535a
> h1
margin 0 0 1em 0
padding 0 0 8px 0
font-size 1em
- color #555
- border-bottom solid 1px #eee
+ color isDark ? #e3e7ea : #555
+ border-bottom solid 1px isDark ? #1c2023 : #eee
&, >>> *
.ui.button.block
@@ -418,13 +460,18 @@ export default Vue.extend({
margin 0 0 1em 0
padding 0 0 8px 0
font-size 1em
- color #555
- border-bottom solid 1px #eee
+ color isDark ? #e3e7ea : #555
+ border-bottom solid 1px isDark ? #1c2023 : #eee
> .web
> .div
- border-bottom solid 1px #eee
- padding 0 0 16px 0
- margin 0 0 16px 0
+ border-bottom solid 1px isDark ? #1c2023 : #eee
+ margin 16px 0
+
+.mk-settings[data-darkmode]
+ root(true)
+
+.mk-settings:not([data-darkmode])
+ root(false)
</style>
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 51ee93cba6..dd4012039b 100644
--- a/src/client/app/desktop/views/components/sub-note-content.vue
+++ b/src/client/app/desktop/views/components/sub-note-content.vue
@@ -1,6 +1,7 @@
<template>
<div class="mk-sub-note-content">
<div class="body">
+ <span v-if="note.isHidden" style="opacity: 0.5">(この投稿は非公開です)</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>
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue
index 1e98f087e1..254a5b9d63 100644
--- a/src/client/app/desktop/views/components/timeline.core.vue
+++ b/src/client/app/desktop/views/components/timeline.core.vue
@@ -1,24 +1,23 @@
<template>
-<div class="mk-home-timeline">
+<div class="mk-timeline-core">
<mk-friends-maker v-if="src == 'home' && alone"/>
<div class="fetching" v-if="fetching">
<mk-ellipsis-icon/>
</div>
- <p class="empty" v-if="notes.length == 0 && !fetching">
- %fa:R comments%%i18n:@empty%
- </p>
- <mk-notes :notes="notes" ref="timeline">
- <button slot="footer" @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
- <template v-if="!moreFetching">%i18n:@load-more%</template>
- <template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
- </button>
+
+ <mk-notes ref="timeline" :more="canFetchMore ? more : null">
+ <p :class="$style.empty" slot="empty">
+ %fa:R comments%%i18n:@empty%
+ </p>
</mk-notes>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
-import { url } from '../../../config';
+import getNoteSummary from '../../../../../renderers/get-note-summary';
+
+const fetchLimit = 10;
export default Vue.extend({
props: {
@@ -33,9 +32,9 @@ export default Vue.extend({
fetching: true,
moreFetching: false,
existMore: false,
- notes: [],
connection: null,
connectionId: null,
+ unreadCount: 0,
date: null
};
},
@@ -59,6 +58,10 @@ export default Vue.extend({
: this.src == 'local'
? 'notes/local-timeline'
: 'notes/global-timeline';
+ },
+
+ canFetchMore(): boolean {
+ return !this.moreFetching && !this.fetching && this.existMore;
}
},
@@ -72,6 +75,9 @@ export default Vue.extend({
this.connection.on('unfollow', this.onChangeFollowing);
}
+ document.addEventListener('keydown', this.onKeydown);
+ document.addEventListener('visibilitychange', this.onVisibilitychange, false);
+
this.fetch();
},
@@ -82,56 +88,62 @@ export default Vue.extend({
this.connection.off('unfollow', this.onChangeFollowing);
}
this.stream.dispose(this.connectionId);
+
+ document.removeEventListener('keydown', this.onKeydown);
+ document.removeEventListener('visibilitychange', this.onVisibilitychange);
},
methods: {
- fetch(cb?) {
+ fetch() {
this.fetching = true;
- (this as any).api(this.endpoint, {
- limit: 11,
- untilDate: this.date ? this.date.getTime() : undefined
- }).then(notes => {
- if (notes.length == 11) {
- notes.pop();
- this.existMore = true;
- }
- this.notes = notes;
- this.fetching = false;
- this.$emit('loaded');
- if (cb) cb();
- });
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api(this.endpoint, {
+ limit: fetchLimit + 1,
+ untilDate: this.date ? this.date.getTime() : undefined,
+ includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+ includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ this.$emit('loaded');
+ }, rej);
+ }));
},
more() {
- if (this.moreFetching || this.fetching || this.notes.length == 0 || !this.existMore) return;
+ if (!this.canFetchMore) return;
+
this.moreFetching = true;
+
(this as any).api(this.endpoint, {
- limit: 11,
- untilId: this.notes[this.notes.length - 1].id
+ limit: fetchLimit + 1,
+ untilId: (this.$refs.timeline as any).tail().id,
+ includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+ includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
}).then(notes => {
- if (notes.length == 11) {
+ if (notes.length == fetchLimit + 1) {
notes.pop();
} else {
this.existMore = false;
}
- this.notes = this.notes.concat(notes);
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
},
onNote(note) {
- // サウンドを再生する
- if ((this as any).os.isEnableSounds) {
- const sound = new Audio(`${url}/assets/post.mp3`);
- sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
- sound.play();
+ if (document.hidden && note.userId !== (this as any).os.i.id) {
+ this.unreadCount++;
+ document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
}
- this.notes.unshift(note);
-
- const isTop = window.scrollY > 8;
- if (isTop) this.notes.pop();
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
},
onChangeFollowing() {
@@ -145,31 +157,51 @@ export default Vue.extend({
warp(date) {
this.date = date;
this.fetch();
+ },
+
+ onVisibilitychange() {
+ if (!document.hidden) {
+ this.unreadCount = 0;
+ document.title = 'Misskey';
+ }
+ },
+
+ onKeydown(e) {
+ if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
+ if (e.which == 84) { // t
+ this.focus();
+ }
+ }
}
}
});
</script>
<style lang="stylus" scoped>
-.mk-home-timeline
+@import '~const.styl'
+
+.mk-timeline-core
> .mk-friends-maker
border-bottom solid 1px #eee
> .fetching
padding 64px 0
- > .empty
- display block
- margin 0 auto
- padding 32px
- max-width 400px
- text-align center
- color #999
+</style>
+
+<style lang="stylus" module>
+.empty
+ display block
+ margin 0 auto
+ padding 32px
+ max-width 400px
+ text-align center
+ color #999
- > [data-fa]
- display block
- margin-bottom 16px
- font-size 3em
- color #ccc
+ > [data-fa]
+ display block
+ margin-bottom 16px
+ font-size 3em
+ color #ccc
</style>
diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index e0215ad1a2..a776e40a24 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -1,19 +1,23 @@
<template>
<div class="mk-timeline">
<header>
- <span :data-is-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span>
- <span :data-is-active="src == 'local'" @click="src = 'local'">%fa:R comments% ローカル</span>
- <span :data-is-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>
+ <span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span>
+ <button @click="chooseList" title="%i18n:@list%">%fa:list%</button>
</header>
<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/>
<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/>
<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/>
+ <mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import XCore from './timeline.core.vue';
+import MkUserListsWindow from './user-lists-window.vue';
export default Vue.extend({
components: {
@@ -22,44 +26,35 @@ export default Vue.extend({
data() {
return {
- src: 'home'
+ src: 'home',
+ list: null
};
},
- mounted() {
- document.addEventListener('keydown', this.onKeydown);
- window.addEventListener('scroll', this.onScroll);
-
- console.log(this.$refs.tl);
+ created() {
+ if ((this as any).os.i.followingCount == 0) {
+ this.src = 'local';
+ }
+ },
+ mounted() {
(this.$refs.tl as any).$once('loaded', () => {
this.$emit('loaded');
});
},
- beforeDestroy() {
- document.removeEventListener('keydown', this.onKeydown);
- window.removeEventListener('scroll', this.onScroll);
- },
-
methods: {
- onScroll() {
- if ((this as any).os.i.clientSettings.fetchOnScroll !== false) {
- const current = window.scrollY + window.innerHeight;
- if (current > document.body.offsetHeight - 8) (this.$refs.tl as any).more();
- }
- },
-
- onKeydown(e) {
- if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
- if (e.which == 84) { // t
- (this.$refs.tl as any).focus();
- }
- }
- },
-
warp(date) {
(this.$refs.tl as any).warp(date);
+ },
+
+ chooseList() {
+ const w = (this as any).os.new(MkUserListsWindow);
+ w.$once('choosen', list => {
+ this.list = list;
+ this.src = 'list';
+ w.close();
+ });
}
}
});
@@ -68,26 +63,68 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-timeline
- background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
+root(isDark)
+ background isDark ? #282C37 : #fff
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
> header
- padding 8px 16px
- border-bottom solid 1px #eee
+ padding 0 8px
+ z-index 10
+ background isDark ? #313543 : #fff
+ border-radius 6px 6px 0 0
+ box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
+
+ > button
+ position absolute
+ z-index 2
+ top 0
+ right 0
+ padding 0
+ width 42px
+ font-size 0.9em
+ line-height 42px
+ color isDark ? #9baec8 : #ccc
+
+ &:hover
+ color isDark ? #b2c1d5 : #aaa
+
+ &:active
+ color isDark ? #b2c1d5 : #999
> span
- margin-right 16px
- line-height 27px
- font-size 14px
- color #555
+ display inline-block
+ padding 0 10px
+ line-height 42px
+ font-size 12px
+ user-select none
- &:not([data-is-active])
+ &[data-active]
color $theme-color
+ cursor default
+ font-weight bold
+
+ &:before
+ content ""
+ display block
+ position absolute
+ bottom 0
+ left -8px
+ width calc(100% + 16px)
+ height 2px
+ background $theme-color
+
+ &:not([data-active])
+ color isDark ? #9aa2a7 : #6f7477
cursor pointer
&:hover
- text-decoration underline
+ color isDark ? #d9dcde : #525a5f
+
+.mk-timeline[data-darkmode]
+ root(true)
+
+.mk-timeline:not([data-darkmode])
+ root(false)
</style>
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 558aaa6dc8..fd15ea6006 100644
--- a/src/client/app/desktop/views/components/ui.header.account.vue
+++ b/src/client/app/desktop/views/components/ui.header.account.vue
@@ -2,32 +2,40 @@
<div class="account">
<button class="header" :data-active="isOpen" @click="toggle">
<span class="username">{{ os.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
- <img class="avatar" :src="`${ os.i.avatarUrl }?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="os.i"/>
</button>
<transition name="zoom-in-top">
<div class="menu" v-if="isOpen">
<ul>
<li>
- <router-link :to="`/@${ os.i.username }`">%fa:user%%i18n:@profile%%fa:angle-right%</router-link>
+ <router-link :to="`/@${ os.i.username }`">%fa:user%<span>%i18n:@profile%</span>%fa:angle-right%</router-link>
</li>
<li @click="drive">
- <p>%fa:cloud%%i18n:@drive%%fa:angle-right%</p>
+ <p>%fa:cloud%<span>%i18n:@drive%</span>%fa:angle-right%</p>
</li>
<li>
- <a href="/i/mentions">%fa:at%%i18n:@mentions%%fa:angle-right%</a>
+ <router-link to="/i/favorites">%fa:star%<span>%i18n:@favorites%</span>%fa:angle-right%</router-link>
+ </li>
+ <li @click="list">
+ <p>%fa:list%<span>%i18n:@lists%</span>%fa:angle-right%</p>
</li>
</ul>
<ul>
<li>
- <a href="/i/customize-home">%fa:wrench%%i18n:@customize%%fa:angle-right%</a>
+ <router-link to="/i/customize-home">%fa:wrench%<span>%i18n:@customize%</span>%fa:angle-right%</router-link>
</li>
<li @click="settings">
- <p>%fa:cog%%i18n:@settings%%fa:angle-right%</p>
+ <p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p>
</li>
</ul>
<ul>
<li @click="signout">
- <p>%fa:power-off%%i18n:@signout%%fa:angle-right%</p>
+ <p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p>
+ </li>
+ </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>
</li>
</ul>
</div>
@@ -37,6 +45,7 @@
<script lang="ts">
import Vue from 'vue';
+import MkUserListsWindow from './user-lists-window.vue';
import MkSettingsWindow from './settings-window.vue';
import MkDriveWindow from './drive-window.vue';
import contains from '../../../common/scripts/contains';
@@ -75,12 +84,22 @@ export default Vue.extend({
this.close();
(this as any).os.new(MkDriveWindow);
},
+ list() {
+ this.close();
+ const w = (this as any).os.new(MkUserListsWindow);
+ w.$once('choosen', list => {
+ this.$router.push(`i/lists/${ list.id }`);
+ });
+ },
settings() {
this.close();
(this as any).os.new(MkSettingsWindow);
},
signout() {
(this as any).os.signout();
+ },
+ dark() {
+ (this as any)._updateDarkmode_(!(this as any)._darkmode_);
}
}
});
@@ -89,7 +108,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.account
+root(isDark)
> .header
display block
margin 0
@@ -104,13 +123,13 @@ export default Vue.extend({
&:hover
&[data-active='true']
- color darken(#9eaba8, 20%)
+ color isDark ? #fff : darken(#9eaba8, 20%)
> .avatar
filter saturate(150%)
&:active
- color darken(#9eaba8, 30%)
+ color isDark ? #fff : darken(#9eaba8, 30%)
> .username
display block
@@ -137,15 +156,16 @@ export default Vue.extend({
transition filter 100ms ease
> .menu
+ $bgcolor = isDark ? #282c37 : #fff
display block
position absolute
top 56px
right -2px
width 230px
font-size 0.8em
- background #fff
+ background $bgcolor
border-radius 4px
- box-shadow 0 1px 4px rgba(0, 0, 0, 0.25)
+ box-shadow 0 1px 4px rgba(#000, 0.25)
&:before
content ""
@@ -156,7 +176,7 @@ export default Vue.extend({
right 12px
border-top solid 14px transparent
border-right solid 14px transparent
- border-bottom solid 14px rgba(0, 0, 0, 0.1)
+ border-bottom solid 14px rgba(#000, 0.1)
border-left solid 14px transparent
&:after
@@ -168,7 +188,7 @@ export default Vue.extend({
right 12px
border-top solid 14px transparent
border-right solid 14px transparent
- border-bottom solid 14px #fff
+ border-bottom solid 14px $bgcolor
border-left solid 14px transparent
ul
@@ -179,7 +199,7 @@ export default Vue.extend({
& + ul
padding-top 10px
- border-top solid 1px #eee
+ border-top solid 1px isDark ? #1c2023 : #eee
> li
display block
@@ -193,16 +213,20 @@ export default Vue.extend({
padding 0 28px
margin 0
line-height 40px
- color #868C8C
+ color isDark ? #c8cece : #868C8C
cursor pointer
*
pointer-events none
- > [data-fa]:first-of-type
+ > span:first-child
+ padding-left 22px
+
+ > [data-fa]:first-child
margin-right 6px
+ width 16px
- > [data-fa]:last-of-type
+ > [data-fa]:last-child
display block
position absolute
top 0
@@ -220,9 +244,25 @@ export default Vue.extend({
&:active
background darken($theme-color, 10%)
+ &.signout
+ $color = #e64137
+
+ &:hover, &:active
+ background $color
+ color #fff
+
+ &:active
+ background darken($color, 10%)
+
.zoom-in-top-enter-active,
.zoom-in-top-leave-active {
transform-origin: center -16px;
}
+.account[data-darkmode]
+ root(true)
+
+.account:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue
index 19f72a86d7..0800d96eb6 100644
--- a/src/client/app/desktop/views/components/ui.header.nav.vue
+++ b/src/client/app/desktop/views/components/ui.header.nav.vue
@@ -99,7 +99,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.nav
+root(isDark)
display inline-block
margin 0
padding 0
@@ -131,7 +131,7 @@ export default Vue.extend({
padding 0 24px
font-size 13px
font-variant small-caps
- color #9eaba8
+ color isDark ? #b8c5ca : #9eaba8
text-decoration none
transition none
cursor pointer
@@ -140,7 +140,7 @@ export default Vue.extend({
pointer-events none
&:hover
- color darken(#9eaba8, 20%)
+ color isDark ? #fff : darken(#9eaba8, 20%)
text-decoration none
> [data-fa]:first-child
@@ -164,4 +164,10 @@ export default Vue.extend({
@media (max-width 700px)
padding 0 12px
+.nav[data-darkmode]
+ root(true)
+
+.nav:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/components/ui.header.notifications.vue b/src/client/app/desktop/views/components/ui.header.notifications.vue
index e9a6b9b04f..ea814dd7a3 100644
--- a/src/client/app/desktop/views/components/ui.header.notifications.vue
+++ b/src/client/app/desktop/views/components/ui.header.notifications.vue
@@ -84,7 +84,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.notifications
+root(isDark)
> button
display block
@@ -101,10 +101,10 @@ export default Vue.extend({
&:hover
&[data-active='true']
- color darken(#9eaba8, 20%)
+ color isDark ? #fff : darken(#9eaba8, 20%)
&:active
- color darken(#9eaba8, 30%)
+ color isDark ? #fff : darken(#9eaba8, 30%)
> [data-fa].bell
font-size 1.2em
@@ -117,14 +117,15 @@ export default Vue.extend({
color $theme-color
> .pop
+ $bgcolor = isDark ? #282c37 : #fff
display block
position absolute
top 56px
right -72px
width 300px
- background #fff
+ background $bgcolor
border-radius 4px
- box-shadow 0 1px 4px rgba(0, 0, 0, 0.25)
+ box-shadow 0 1px 4px rgba(#000, 0.25)
&:before
content ""
@@ -135,7 +136,7 @@ export default Vue.extend({
right 74px
border-top solid 14px transparent
border-right solid 14px transparent
- border-bottom solid 14px rgba(0, 0, 0, 0.1)
+ border-bottom solid 14px rgba(#000, 0.1)
border-left solid 14px transparent
&:after
@@ -147,7 +148,7 @@ export default Vue.extend({
right 74px
border-top solid 14px transparent
border-right solid 14px transparent
- border-bottom solid 14px #fff
+ border-bottom solid 14px $bgcolor
border-left solid 14px transparent
> .mk-notifications
@@ -155,4 +156,10 @@ export default Vue.extend({
font-size 1rem
overflow auto
+.notifications[data-darkmode]
+ root(true)
+
+.notifications:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/components/ui.header.search.vue b/src/client/app/desktop/views/components/ui.header.search.vue
index 3167aab8ab..1ed28ba3a8 100644
--- a/src/client/app/desktop/views/components/ui.header.search.vue
+++ b/src/client/app/desktop/views/components/ui.header.search.vue
@@ -50,7 +50,7 @@ export default Vue.extend({
width 14em
height 32px
font-size 1em
- background rgba(0, 0, 0, 0.05)
+ background rgba(#000, 0.05)
outline none
//border solid 1px #ddd
border none
@@ -62,7 +62,7 @@ export default Vue.extend({
color #9eaba8
&:hover
- background rgba(0, 0, 0, 0.08)
+ background rgba(#000, 0.08)
&:focus
box-shadow 0 0 0 2px rgba($theme-color, 0.5) !important
diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
index 2b63030cd2..7729575b56 100644
--- a/src/client/app/desktop/views/components/ui.header.vue
+++ b/src/client/app/desktop/views/components/ui.header.vue
@@ -43,10 +43,13 @@ export default Vue.extend({
XClock,
},
mounted() {
+ this.$store.commit('setUiHeaderHeight', 48);
+
if ((this as any).os.isSignedIn) {
- const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000
+ const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000;
const isHisasiburi = ago >= 3600;
(this as any).os.i.lastUsedAt = new Date();
+ (this as any).os.bakeMe();
if (isHisasiburi) {
(this.$refs.welcomeback as any).style.display = 'block';
(this.$refs.main as any).style.overflow = 'hidden';
@@ -101,7 +104,7 @@ root(isDark)
top 0
z-index 1000
width 100%
- box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
+ box-shadow 0 1px 1px rgba(#000, 0.075)
> .main
height 48px
@@ -130,7 +133,7 @@ root(isDark)
line-height 48px
margin 0
text-align center
- color #888
+ color isDark ? #fff : #888
opacity 0
> .container
@@ -169,10 +172,10 @@ root(isDark)
> .mk-ui-header-search
display none
-.header[data-is-darkmode]
+.header[data-darkmode]
root(true)
-.header
+.header:not([data-darkmode])
root(false)
</style>
diff --git a/src/client/app/desktop/views/components/user-list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue
new file mode 100644
index 0000000000..59d6abbbc1
--- /dev/null
+++ b/src/client/app/desktop/views/components/user-list-timeline.vue
@@ -0,0 +1,93 @@
+<template>
+<div>
+ <mk-notes ref="timeline" :more="existMore ? more : null"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { UserListStream } from '../../../common/scripts/streaming/user-list';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ props: ['list'],
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null
+ };
+ },
+ watch: {
+ $route: 'init'
+ },
+ mounted() {
+ this.init();
+ },
+ beforeDestroy() {
+ this.connection.close();
+ },
+ methods: {
+ init() {
+ if (this.connection) this.connection.close();
+ this.connection = new UserListStream((this as any).os, (this as any).os.i, this.list.id);
+ this.connection.on('note', this.onNote);
+ this.connection.on('userAdded', this.onUserAdded);
+ this.connection.on('userRemoved', this.onUserRemoved);
+
+ this.fetch();
+ },
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('notes/user-list-timeline', {
+ listId: this.list.id,
+ limit: fetchLimit + 1,
+ includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+ includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ this.$emit('loaded');
+ }, rej);
+ }));
+ },
+ more() {
+ this.moreFetching = true;
+
+ (this as any).api('notes/user-list-timeline', {
+ listId: this.list.id,
+ limit: fetchLimit + 1,
+ untilId: (this.$refs.timeline as any).tail().id,
+ includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+ includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
+ this.moreFetching = false;
+ });
+ },
+ onNote(note) {
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
+ },
+ onUserAdded() {
+ this.fetch();
+ },
+ onUserRemoved() {
+ this.fetch();
+ }
+ }
+});
+</script>
diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue
new file mode 100644
index 0000000000..d082610132
--- /dev/null
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -0,0 +1,69 @@
+<template>
+<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_">
+ <button class="ui" @click="add">リストを作成</button>
+ <a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a>
+ </div>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+ data() {
+ return {
+ fetching: true,
+ lists: []
+ };
+ },
+ mounted() {
+ (this as any).api('users/lists/list').then(lists => {
+ this.fetching = false;
+ this.lists = lists;
+ });
+ },
+ methods: {
+ add() {
+ (this as any).apis.input({
+ title: 'リスト名',
+ }).then(async title => {
+ const list = await (this as any).api('users/lists/create', {
+ title
+ });
+
+ this.$emit('choosen', list);
+ });
+ },
+ choice(list) {
+ this.$emit('choosen', list);
+ },
+ close() {
+ (this as any).$refs.window.close();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+
+root(isDark)
+ padding 16px
+
+ > button
+ margin-bottom 16px
+
+ > a
+ display block
+ padding 16px
+ border solid 1px isDark ? #1c2023 : #eee
+ border-radius 4px
+
+[data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82"][data-darkmode]
+ root(true)
+
+[data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82"]:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index bcd79dc2af..cc5e021390 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -2,11 +2,9 @@
<div class="mk-user-preview">
<template v-if="u != null">
<div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl}?thumbnail&size=512)` : ''"></div>
- <router-link class="avatar" :to="u | userPage">
- <img :src="`${u.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
- </router-link>
+ <mk-avatar class="avatar" :user="u" :disable-preview="true"/>
<div class="title">
- <router-link class="name" :to="u | userPage">{{ u.name }}</router-link>
+ <router-link class="name" :to="u | userPage">{{ u | userName }}</router-link>
<p class="username">@{{ u | acct }}</p>
</div>
<div class="description">{{ u.description }}</div>
@@ -87,21 +85,21 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-user-preview
+root(isDark)
position absolute
z-index 2048
margin-top -8px
width 250px
- background #fff
+ background isDark ? #282c37 : #fff
background-clip content-box
- border solid 1px rgba(0, 0, 0, 0.1)
+ border solid 1px rgba(#000, 0.1)
border-radius 4px
overflow hidden
opacity 0
> .banner
height 84px
- background-color #f5f5f5
+ background-color isDark ? #1c1e26 : #f5f5f5
background-size cover
background-position center
@@ -111,14 +109,10 @@ export default Vue.extend({
top 62px
left 13px
z-index 2
-
- > img
- display block
- width 58px
- height 58px
- margin 0
- border solid 3px #fff
- border-radius 8px
+ width 58px
+ height 58px
+ border solid 3px isDark ? #282c37 : #fff
+ border-radius 8px
> .title
display block
@@ -129,19 +123,19 @@ export default Vue.extend({
margin 0
font-weight bold
line-height 16px
- color #656565
+ color isDark ? #fff : #656565
> .username
display block
margin 0
line-height 16px
font-size 0.8em
- color #999
+ color isDark ? #606984 : #999
> .description
padding 0 16px
font-size 0.7em
- color #555
+ color isDark ? #9ea4ad : #555
> .status
padding 8px 16px
@@ -164,4 +158,10 @@ export default Vue.extend({
top 92px
right 8px
+.mk-user-preview[data-darkmode]
+ root(true)
+
+.mk-user-preview:not([data-darkmode])
+ root(false)
+
</style>
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 005c9cd6d3..dbad295178 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -1,8 +1,6 @@
<template>
<div class="root item">
- <router-link class="avatar-anchor" :to="user | userPage" v-user-preview="user.id">
- <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
- </router-link>
+ <mk-avatar class="avatar" :user="user"/>
<div class="main">
<header>
<router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link>
@@ -35,18 +33,13 @@ export default Vue.extend({
display block
clear both
- > .avatar-anchor
+ > .avatar
display block
float left
margin 0 16px 0 0
-
- > .avatar
- display block
- width 58px
- height 58px
- margin 0
- border-radius 8px
- vertical-align bottom
+ width 58px
+ height 58px
+ border-radius 8px
> .main
float left
diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue
index a08e76f573..13d0d07bbc 100644
--- a/src/client/app/desktop/views/components/users-list.vue
+++ b/src/client/app/desktop/views/components/users-list.vue
@@ -2,8 +2,8 @@
<div class="mk-users-list">
<nav>
<div>
- <span :data-is-active="mode == 'all'" @click="mode = 'all'">すべて<span>{{ count }}</span></span>
- <span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span>
+ <span :data-active="mode == 'all'" @click="mode = 'all'">すべて<span>{{ count }}</span></span>
+ <span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">知り合い<span>{{ youKnowCount }}</span></span>
</div>
</nav>
<div class="users" v-if="!fetching && users.length != 0">
@@ -98,7 +98,7 @@ export default Vue.extend({
*
pointer-events none
- &[data-is-active]
+ &[data-active]
font-weight bold
color $theme-color
border-color $theme-color
@@ -119,7 +119,7 @@ export default Vue.extend({
overflow auto
> *
- border-bottom solid 1px rgba(0, 0, 0, 0.05)
+ border-bottom solid 1px rgba(#000, 0.05)
> *
max-width 600px
diff --git a/src/client/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue
index 188a67313e..ab8327d39e 100644
--- a/src/client/app/desktop/views/components/widget-container.vue
+++ b/src/client/app/desktop/views/components/widget-container.vue
@@ -24,8 +24,8 @@ export default Vue.extend({
computed: {
withGradient(): boolean {
return (this as any).os.isSignedIn
- ? (this as any).os.i.clientSettings.gradientWindowHeader != null
- ? (this as any).os.i.clientSettings.gradientWindowHeader
+ ? (this as any).clientSettings.gradientWindowHeader != null
+ ? (this as any).clientSettings.gradientWindowHeader
: false
: false;
}
@@ -34,9 +34,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-widget-container
- background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
+root(isDark)
+ background isDark ? #282C37 : #fff
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
overflow hidden
@@ -45,6 +45,8 @@ export default Vue.extend({
border none !important
> header
+ background isDark ? #313543 : #fff
+
> .title
z-index 1
margin 0
@@ -52,11 +54,11 @@ export default Vue.extend({
line-height 42px
font-size 0.9em
font-weight bold
- color #888
- box-shadow 0 1px rgba(0, 0, 0, 0.07)
+ color isDark ? #e3e5e8 : #888
+ box-shadow 0 1px rgba(#000, 0.07)
> [data-fa]
- margin-right 4px
+ margin-right 6px
&:empty
display none
@@ -70,16 +72,23 @@ export default Vue.extend({
width 42px
font-size 0.9em
line-height 42px
- color #ccc
+ color isDark ? #9baec8 : #ccc
&:hover
- color #aaa
+ color isDark ? #b2c1d5 : #aaa
&:active
- color #999
+ color isDark ? #b2c1d5 : #999
&.withGradient
> .title
- background linear-gradient(to bottom, #fff, #ececec)
+ background isDark ? linear-gradient(to bottom, #313543, #1d2027) : linear-gradient(to bottom, #fff, #ececec)
box-shadow 0 1px rgba(#000, 0.11)
+
+.mk-widget-container[data-darkmode]
+ root(true)
+
+.mk-widget-container:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
index e2cab21799..2e7eb557b4 100644
--- a/src/client/app/desktop/views/components/window.vue
+++ b/src/client/app/desktop/views/components/window.vue
@@ -4,7 +4,7 @@
<div class="main" ref="main" tabindex="-1" :data-is-modal="isModal" @mousedown="onBodyMousedown" @keydown="onKeydown" :style="{ width, height }">
<div class="body">
<header ref="header"
- :class="{ withGradient }"
+ :class="{ withGradient: clientSettings.gradientWindowHeader }"
@contextmenu.prevent="() => {}" @mousedown.prevent="onHeaderMousedown"
>
<h1><slot name="header"></slot></h1>
@@ -17,14 +17,16 @@
<slot></slot>
</div>
</div>
- <div class="handle top" v-if="canResize" @mousedown.prevent="onTopHandleMousedown"></div>
- <div class="handle right" v-if="canResize" @mousedown.prevent="onRightHandleMousedown"></div>
- <div class="handle bottom" v-if="canResize" @mousedown.prevent="onBottomHandleMousedown"></div>
- <div class="handle left" v-if="canResize" @mousedown.prevent="onLeftHandleMousedown"></div>
- <div class="handle top-left" v-if="canResize" @mousedown.prevent="onTopLeftHandleMousedown"></div>
- <div class="handle top-right" v-if="canResize" @mousedown.prevent="onTopRightHandleMousedown"></div>
- <div class="handle bottom-right" v-if="canResize" @mousedown.prevent="onBottomRightHandleMousedown"></div>
- <div class="handle bottom-left" v-if="canResize" @mousedown.prevent="onBottomLeftHandleMousedown"></div>
+ <template v-if="canResize">
+ <div class="handle top" @mousedown.prevent="onTopHandleMousedown"></div>
+ <div class="handle right" @mousedown.prevent="onRightHandleMousedown"></div>
+ <div class="handle bottom" @mousedown.prevent="onBottomHandleMousedown"></div>
+ <div class="handle left" @mousedown.prevent="onLeftHandleMousedown"></div>
+ <div class="handle top-left" @mousedown.prevent="onTopLeftHandleMousedown"></div>
+ <div class="handle top-right" @mousedown.prevent="onTopRightHandleMousedown"></div>
+ <div class="handle bottom-right" @mousedown.prevent="onBottomRightHandleMousedown"></div>
+ <div class="handle bottom-left" @mousedown.prevent="onBottomLeftHandleMousedown"></div>
+ </template>
</div>
</div>
</template>
@@ -85,17 +87,10 @@ export default Vue.extend({
computed: {
isFlexible(): boolean {
- return this.height == null;
+ return this.height == 'auto';
},
canResize(): boolean {
return !this.isFlexible;
- },
- withGradient(): boolean {
- return (this as any).os.isSignedIn
- ? (this as any).os.i.clientSettings.gradientWindowHeader != null
- ? (this as any).os.i.clientSettings.gradientWindowHeader
- : false
- : false;
}
},
@@ -465,7 +460,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-window
+root(isDark)
display block
> .bg
@@ -476,7 +471,7 @@ export default Vue.extend({
left 0
width 100%
height 100%
- background rgba(0, 0, 0, 0.7)
+ background rgba(#000, 0.7)
opacity 0
pointer-events none
@@ -493,7 +488,7 @@ export default Vue.extend({
&:focus
&:not([data-is-modal])
> .body
- box-shadow 0 0 0px 1px rgba($theme-color, 0.5), 0 2px 6px 0 rgba(0, 0, 0, 0.2)
+ box-shadow 0 0 0px 1px rgba($theme-color, 0.5), 0 2px 6px 0 rgba(#000, 0.2)
> .handle
$size = 8px
@@ -559,9 +554,9 @@ export default Vue.extend({
> .body
height 100%
overflow hidden
- background #fff
+ background isDark ? #282C37 : #fff
border-radius 6px
- box-shadow 0 2px 6px 0 rgba(0, 0, 0, 0.2)
+ box-shadow 0 2px 6px 0 rgba(#000, 0.2)
> header
$header-height = 40px
@@ -571,12 +566,12 @@ export default Vue.extend({
overflow hidden
white-space nowrap
cursor move
- background #fff
+ background isDark ? #313543 : #fff
border-radius 6px 6px 0 0
box-shadow 0 1px 0 rgba(#000, 0.1)
&.withGradient
- background linear-gradient(to bottom, #fff, #ececec)
+ background isDark ? linear-gradient(to bottom, #313543, #1d2027) : linear-gradient(to bottom, #fff, #ececec)
box-shadow 0 1px 0 rgba(#000, 0.15)
&, *
@@ -593,7 +588,7 @@ export default Vue.extend({
font-size 1em
line-height $header-height
font-weight normal
- color #666
+ color isDark ? #e3e5e8 : #666
> div:last-child
position absolute
@@ -608,16 +603,16 @@ export default Vue.extend({
padding 0
cursor pointer
font-size 1em
- color rgba(#000, 0.4)
+ color isDark ? #9baec8 : rgba(#000, 0.4)
border none
outline none
background transparent
&:hover
- color rgba(#000, 0.6)
+ color isDark ? #b2c1d5 : rgba(#000, 0.6)
&:active
- color darken(#000, 30%)
+ color isDark ? #b2c1d5 : darken(#000, 30%)
> [data-fa]
padding 0
@@ -632,4 +627,10 @@ export default Vue.extend({
> .main > .body > .content
height calc(100% - 40px)
+.mk-window[data-darkmode]
+ root(true)
+
+.mk-window:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/pages/favorites.vue b/src/client/app/desktop/views/pages/favorites.vue
new file mode 100644
index 0000000000..d908c08f7c
--- /dev/null
+++ b/src/client/app/desktop/views/pages/favorites.vue
@@ -0,0 +1,73 @@
+<template>
+<mk-ui>
+ <main v-if="!fetching">
+ <template v-for="favorite in favorites">
+ <mk-note-detail :note="favorite.note" :key="favorite.note.id"/>
+ </template>
+ <a v-if="existMore" @click="more">さらに読み込む</a>
+ </main>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+ data() {
+ return {
+ fetching: true,
+ favorites: [],
+ existMore: false,
+ moreFetching: false
+ };
+ },
+ created() {
+ this.fetch();
+ },
+ methods: {
+ fetch() {
+ Progress.start();
+ this.fetching = true;
+
+ (this as any).api('i/favorites', {
+ limit: 11
+ }).then(favorites => {
+ if (favorites.length == 11) {
+ this.existMore = true;
+ favorites.pop();
+ }
+
+ this.favorites = favorites;
+ this.fetching = false;
+
+ Progress.done();
+ });
+ },
+ more() {
+ this.moreFetching = true;
+ (this as any).api('i/favorites', {
+ limit: 11,
+ maxId: this.favorites[this.favorites.length - 1].id
+ }).then(favorites => {
+ if (favorites.length == 11) {
+ this.existMore = true;
+ favorites.pop();
+ } else {
+ this.existMore = false;
+ }
+
+ this.favorites = this.favorites.concat(favorites);
+ this.moreFetching = false;
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+main
+ margin 0 auto
+ padding 16px
+ max-width 700px
+</style>
diff --git a/src/client/app/desktop/views/pages/note.vue b/src/client/app/desktop/views/pages/note.vue
index e92b0ff105..8502dd3d58 100644
--- a/src/client/app/desktop/views/pages/note.vue
+++ b/src/client/app/desktop/views/pages/note.vue
@@ -1,9 +1,11 @@
<template>
<mk-ui>
<main v-if="!fetching">
- <a v-if="note.next" :href="note.next">%fa:angle-up%%i18n:@next%</a>
<mk-note-detail :note="note"/>
- <a v-if="note.prev" :href="note.prev">%fa:angle-down%%i18n:@prev%</a>
+ <footer>
+ <router-link v-if="note.next" :to="note.next">%fa:angle-left% %i18n:@next%</router-link>
+ <router-link v-if="note.prev" :to="note.prev">%i18n:@prev% %fa:angle-right%</router-link>
+ </footer>
</main>
</mk-ui>
</template>
@@ -48,17 +50,12 @@ main
padding 16px
text-align center
- > a
- display inline-block
+ > footer
+ margin-top 16px
- &:first-child
- margin-bottom 4px
-
- &:last-child
- margin-top 4px
-
- > [data-fa]
- margin-right 4px
+ > a
+ display inline-block
+ margin 0 16px
> .mk-note-detail
margin 0 auto
diff --git a/src/client/app/desktop/views/pages/search.vue b/src/client/app/desktop/views/pages/search.vue
index 698154e667..67e1e3bfe0 100644
--- a/src/client/app/desktop/views/pages/search.vue
+++ b/src/client/app/desktop/views/pages/search.vue
@@ -114,7 +114,7 @@ export default Vue.extend({
.notes
max-width 600px
margin 0 auto
- border solid 1px rgba(0, 0, 0, 0.075)
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
overflow hidden
diff --git a/src/client/app/desktop/views/pages/user-list.users.vue b/src/client/app/desktop/views/pages/user-list.users.vue
new file mode 100644
index 0000000000..4236cdbb14
--- /dev/null
+++ b/src/client/app/desktop/views/pages/user-list.users.vue
@@ -0,0 +1,124 @@
+<template>
+<div>
+ <mk-widget-container>
+ <template slot="header">%fa:users% ユーザー</template>
+ <button slot="func" title="ユーザーを追加" @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>
+ <template v-else-if="users.length != 0">
+ <div class="user" v-for="_user in users">
+ <mk-avatar class="avatar" :user="_user"/>
+ <div class="body">
+ <router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link>
+ <p class="username">@{{ _user | acct }}</p>
+ </div>
+ </div>
+ </template>
+ <p class="empty" v-else>%i18n:@no-one%</p>
+ </div>
+ </mk-widget-container>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ list: {
+ type: Object,
+ required: true
+ }
+ },
+ data() {
+ return {
+ fetching: true,
+ users: []
+ };
+ },
+ mounted() {
+ (this as any).api('users/show', {
+ userIds: this.list.userIds
+ }).then(users => {
+ this.users = users;
+ this.fetching = false;
+ });
+ },
+ methods: {
+ add() {
+ (this as any).apis.input({
+ title: 'ユーザー名',
+ }).then(async username => {
+ const user = await (this as any).api('users/show', {
+ username
+ });
+
+ (this as any).api('users/lists/push', {
+ listId: this.list.id,
+ userId: user.id
+ });
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+ > .user
+ padding 16px
+ border-bottom solid 1px isDark ? #1c2023 : #eee
+
+ &:last-child
+ border-bottom none
+
+ &:after
+ content ""
+ display block
+ clear both
+
+ > .avatar
+ display block
+ float left
+ margin 0 12px 0 0
+ width 42px
+ height 42px
+ border-radius 8px
+
+ > .body
+ float left
+ width calc(100% - 54px)
+
+ > .name
+ margin 0
+ font-size 16px
+ line-height 24px
+ color isDark ? #fff : #555
+
+ > .username
+ display block
+ margin 0
+ font-size 15px
+ line-height 16px
+ color isDark ? #606984 : #ccc
+
+ > .empty
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > .fetching
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+[data-id="d0b63759-a822-4556-a5ce-373ab966e08a"][data-darkmode]
+ root(true)
+
+[data-id="d0b63759-a822-4556-a5ce-373ab966e08a"]:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/desktop/views/pages/user-list.vue b/src/client/app/desktop/views/pages/user-list.vue
new file mode 100644
index 0000000000..2241b84e5e
--- /dev/null
+++ b/src/client/app/desktop/views/pages/user-list.vue
@@ -0,0 +1,71 @@
+<template>
+<mk-ui>
+ <div v-if="!fetching" data-id="02010e15-cc48-4245-8636-16078a9b623c">
+ <div>
+ <div><h1>{{ list.title }}</h1></div>
+ <x-users :list="list"/>
+ </div>
+ <main>
+ <mk-user-list-timeline :list="list"/>
+ </main>
+ </div>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XUsers from './user-list.users.vue';
+
+export default Vue.extend({
+ components: {
+ XUsers
+ },
+ data() {
+ return {
+ fetching: true,
+ list: null
+ };
+ },
+ watch: {
+ $route: 'fetch'
+ },
+ mounted() {
+ this.fetch();
+ },
+ methods: {
+ fetch() {
+ this.fetching = true;
+
+ (this as any).api('users/lists/show', {
+ listId: this.$route.params.list
+ }).then(list => {
+ this.list = list;
+ this.fetching = false;
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+[data-id="02010e15-cc48-4245-8636-16078a9b623c"]
+ display flex
+ justify-content center
+ margin 0 auto
+ max-width 1200px
+
+ > main
+ > div > div
+ > *:not(:last-child)
+ margin-bottom 16px
+
+ > main
+ padding 16px
+ width calc(100% - 275px * 2)
+
+ > div
+ width 275px
+ margin 0
+ padding 16px 0 16px 16px
+
+</style>
diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
index 9ccbc7a310..4c1b91e7a6 100644
--- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
+++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
@@ -38,7 +38,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
.followers-you-know
background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
> .title
@@ -49,7 +49,7 @@ export default Vue.extend({
font-size 0.9em
font-weight bold
color #888
- box-shadow 0 1px rgba(0, 0, 0, 0.07)
+ box-shadow 0 1px rgba(#000, 0.07)
> i
margin-right 4px
diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue
index 203f936478..4af0f0bca6 100644
--- a/src/client/app/desktop/views/pages/user/user.friends.vue
+++ b/src/client/app/desktop/views/pages/user/user.friends.vue
@@ -4,9 +4,7 @@
<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
<template v-if="!fetching && users.length != 0">
<div class="user" v-for="friend in users">
- <router-link class="avatar-anchor" :to="friend | userPage">
- <img class="avatar" :src="`${friend.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/>
- </router-link>
+ <mk-avatar class="avatar" :user="friend"/>
<div class="body">
<router-link class="name" :to="friend | userPage" v-user-preview="friend.id">{{ friend.name }}</router-link>
<p class="username">@{{ friend | acct }}</p>
@@ -44,7 +42,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
.friends
background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
> .title
@@ -55,7 +53,7 @@ export default Vue.extend({
font-size 0.9em
font-weight bold
color #888
- box-shadow 0 1px rgba(0, 0, 0, 0.07)
+ box-shadow 0 1px rgba(#000, 0.07)
> i
margin-right 4px
@@ -82,18 +80,13 @@ export default Vue.extend({
display block
clear both
- > .avatar-anchor
+ > .avatar
display block
float left
margin 0 12px 0 0
-
- > .avatar
- display block
- width 42px
- height 42px
- margin 0
- border-radius 8px
- vertical-align bottom
+ width 42px
+ height 42px
+ border-radius 8px
> .body
float left
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index 7a0672d3d7..60dc15b15d 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -1,12 +1,13 @@
<template>
<div class="header" :data-is-dark-background="user.bannerUrl != null">
- <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>
- <div class="banner-container" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=2048)` : ''">
- <div class="banner" ref="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=2048)` : ''" @click="onBannerClick"></div>
+ <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>
+ <div class="banner-container" :style="style">
+ <div class="banner" ref="banner" :style="style" @click="onBannerClick"></div>
+ <div class="fade"></div>
</div>
- <div class="fade"></div>
<div class="container">
- <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="user" :disable-preview="true"/>
<div class="title">
<p class="name">{{ user | userName }}</p>
<p class="username">@{{ user | acct }}</p>
@@ -24,6 +25,15 @@ import Vue from 'vue';
export default Vue.extend({
props: ['user'],
+ computed: {
+ style(): any {
+ if (this.user.bannerUrl == null) return {};
+ return {
+ backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null,
+ backgroundImage: `url(${ this.user.bannerUrl })`
+ };
+ }
+ },
mounted() {
if (this.user.bannerUrl) {
window.addEventListener('load', this.onScroll);
@@ -67,21 +77,27 @@ export default Vue.extend({
@import '~const.styl'
.header
- $banner-height = 320px
$footer-height = 58px
overflow hidden
background #f7f7f7
- box-shadow 0 1px 1px rgba(0, 0, 0, 0.075)
+ box-shadow 0 1px 1px rgba(#000, 0.075)
+ > .is-suspended
> .is-remote
- padding 16px
- color #573c08
- background #fff0db
+ &.is-suspended
+ color #570808
+ background #ffdbdb
+
+ &.is-remote
+ color #573c08
+ background #fff0db
> p
margin 0 auto
- max-width 1024px
+ padding 14px 16px
+ max-width 1200px
+ font-size 14px
> a
font-weight bold
@@ -91,8 +107,8 @@ export default Vue.extend({
> .banner
background-color #383838
- > .fade
- background linear-gradient(transparent, rgba(0, 0, 0, 0.7))
+ > .fade
+ background linear-gradient(transparent, rgba(#000, 0.7))
> .container
> .title
@@ -102,7 +118,7 @@ export default Vue.extend({
text-shadow 0 0 8px #000
> .banner-container
- height $banner-height
+ height 320px
overflow hidden
background-size cover
background-position center
@@ -113,14 +129,12 @@ export default Vue.extend({
background-size cover
background-position center
- > .fade
- $fade-hight = 78px
-
- position absolute
- top ($banner-height - $fade-hight)
- left 0
- width 100%
- height $fade-hight
+ > .fade
+ position absolute
+ bottom 0
+ left 0
+ width 100%
+ height 78px
> .container
max-width 1200px
@@ -134,10 +148,9 @@ export default Vue.extend({
z-index 2
width 160px
height 160px
- margin 0
border solid 3px #fff
border-radius 8px
- box-shadow 1px 1px 3px rgba(0, 0, 0, 0.2)
+ box-shadow 1px 1px 3px rgba(#000, 0.2)
> .title
position absolute
diff --git a/src/client/app/desktop/views/pages/user/user.home.vue b/src/client/app/desktop/views/pages/user/user.home.vue
index 7ca520ea7f..6b242a6129 100644
--- a/src/client/app/desktop/views/pages/user/user.home.vue
+++ b/src/client/app/desktop/views/pages/user/user.home.vue
@@ -65,7 +65,7 @@ export default Vue.extend({
width calc(100% - 275px * 2)
> .timeline
- border solid 1px rgba(0, 0, 0, 0.075)
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
> div
@@ -91,7 +91,7 @@ export default Vue.extend({
font-size 12px
color #aaa
background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
a
diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue
index 9f749d5cc9..01c4c7b31e 100644
--- a/src/client/app/desktop/views/pages/user/user.photos.vue
+++ b/src/client/app/desktop/views/pages/user/user.photos.vue
@@ -41,7 +41,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
.photos
background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
> .title
@@ -52,7 +52,7 @@ export default Vue.extend({
font-size 0.9em
font-weight bold
color #888
- box-shadow 0 1px rgba(0, 0, 0, 0.07)
+ box-shadow 0 1px rgba(#000, 0.07)
> i
margin-right 4px
diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue
index 72750e1b3d..29e49f36a6 100644
--- a/src/client/app/desktop/views/pages/user/user.profile.vue
+++ b/src/client/app/desktop/views/pages/user/user.profile.vue
@@ -3,8 +3,17 @@
<div class="friend-form" v-if="os.isSignedIn && os.i.id != user.id">
<mk-follow-button :user="user" size="big"/>
<p class="followed" v-if="user.isFollowed">%i18n:@follows-you%</p>
- <p v-if="user.isMuted">%i18n:@muted% <a @click="unmute">%i18n:@unmute%</a></p>
- <p v-if="!user.isMuted"><a @click="mute">%i18n:@mute%</a></p>
+ <p class="stalk" v-if="user.isFollowing">
+ <span v-if="user.isStalking">%i18n:@stalking% <a @click="unstalk">%fa:meh% %i18n:@unstalk%</a></span>
+ <span v-if="!user.isStalking"><a @click="stalk">%fa:user-secret% %i18n:@stalk%</a></span>
+ </p>
+ </div>
+ <div class="action-form">
+ <button class="mute ui" @click="user.isMuted ? unmute() : mute()">
+ <span v-if="user.isMuted">%fa:eye% %i18n:@unmute%</span>
+ <span v-if="!user.isMuted">%fa:eye-slash% %i18n:@mute%</span>
+ </button>
+ <button class="mute ui" @click="list">%fa:list% リストに追加</button>
</div>
<div class="description" v-if="user.description">{{ user.description }}</div>
<div class="birthday" v-if="user.host === null && user.profile.birthday">
@@ -26,6 +35,7 @@ import Vue from 'vue';
import * as age from 's-age';
import MkFollowingWindow from '../../components/following-window.vue';
import MkFollowersWindow from '../../components/followers-window.vue';
+import MkUserListsWindow from '../../components/user-lists-window.vue';
export default Vue.extend({
props: ['user'],
@@ -47,6 +57,26 @@ export default Vue.extend({
});
},
+ stalk() {
+ (this as any).api('following/stalk', {
+ userId: this.user.id
+ }).then(() => {
+ this.user.isStalking = true;
+ }, () => {
+ alert('error');
+ });
+ },
+
+ unstalk() {
+ (this as any).api('following/unstalk', {
+ userId: this.user.id
+ }).then(() => {
+ this.user.isStalking = false;
+ }, () => {
+ alert('error');
+ });
+ },
+
mute() {
(this as any).api('mute/create', {
userId: this.user.id
@@ -65,6 +95,21 @@ export default Vue.extend({
}, () => {
alert('error');
});
+ },
+
+ list() {
+ const w = (this as any).os.new(MkUserListsWindow);
+ w.$once('choosen', async list => {
+ w.close();
+ await (this as any).api('users/lists/push', {
+ listId: list.id,
+ userId: this.user.id
+ });
+ (this as any).apis.dialog({
+ title: 'Done!',
+ text: `${this.user.name}を${list.title}に追加しました。`
+ });
+ });
}
}
});
@@ -73,7 +118,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
.profile
background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
> *:first-child
@@ -81,11 +126,9 @@ export default Vue.extend({
> .friend-form
padding 16px
+ text-align center
border-top solid 1px #eee
- > .mk-big-follow-button
- width 100%
-
> .followed
margin 12px 0 0 0
padding 0
@@ -96,6 +139,20 @@ export default Vue.extend({
background #eefaff
border-radius 4px
+ > .stalk
+ margin 12px 0 0 0
+
+ > .action-form
+ padding 16px
+ text-align center
+ border-top solid 1px #eee
+
+ > *
+ width 100%
+
+ &:not(:last-child)
+ margin-bottom 12px
+
> .description
padding 16px
color #555
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 55d6072a9d..9c9840c190 100644
--- a/src/client/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/client/app/desktop/views/pages/user/user.timeline.vue
@@ -1,42 +1,36 @@
<template>
<div class="timeline">
<header>
- <span :data-is-active="mode == 'default'" @click="mode = 'default'">投稿</span>
- <span :data-is-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿と返信</span>
- <span :data-is-active="mode == 'with-media'" @click="mode = 'with-media'">メディア</span>
+ <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>
</header>
<div class="loading" v-if="fetching">
<mk-ellipsis-icon/>
</div>
- <p class="empty" v-if="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p>
- <mk-notes ref="timeline" :notes="notes">
- <div slot="footer">
- <template v-if="!moreFetching">%fa:moon%</template>
- <template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
- </div>
+ <mk-notes ref="timeline" :more="existMore ? more : null">
+ <p class="empty" slot="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p>
</mk-notes>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
+
+const fetchLimit = 10;
+
export default Vue.extend({
props: ['user'],
data() {
return {
fetching: true,
moreFetching: false,
+ existMore: false,
mode: 'default',
unreadCount: 0,
- notes: [],
date: null
};
},
- computed: {
- empty(): boolean {
- return this.notes.length == 0;
- }
- },
watch: {
mode() {
this.fetch();
@@ -44,13 +38,11 @@ export default Vue.extend({
},
mounted() {
document.addEventListener('keydown', this.onDocumentKeydown);
- window.addEventListener('scroll', this.onScroll);
this.fetch(() => this.$emit('loaded'));
},
beforeDestroy() {
document.removeEventListener('keydown', this.onDocumentKeydown);
- window.removeEventListener('scroll', this.onScroll);
},
methods: {
onDocumentKeydown(e) {
@@ -61,36 +53,43 @@ export default Vue.extend({
}
},
fetch(cb?) {
- (this as any).api('users/notes', {
- userId: this.user.id,
- untilDate: this.date ? this.date.getTime() : undefined,
- includeReplies: this.mode == 'with-replies',
- withMedia: this.mode == 'with-media'
- }).then(notes => {
- this.notes = notes;
- this.fetching = false;
- if (cb) cb();
- });
+ this.fetching = true;
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('users/notes', {
+ userId: this.user.id,
+ limit: fetchLimit + 1,
+ untilDate: this.date ? this.date.getTime() : undefined,
+ includeReplies: this.mode == 'with-replies',
+ withMedia: this.mode == 'with-media'
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ if (cb) cb();
+ }, rej);
+ }));
},
more() {
- if (this.moreFetching || this.fetching || this.notes.length == 0) return;
this.moreFetching = true;
(this as any).api('users/notes', {
userId: this.user.id,
+ limit: fetchLimit + 1,
includeReplies: this.mode == 'with-replies',
withMedia: this.mode == 'with-media',
- untilId: this.notes[this.notes.length - 1].id
+ untilId: (this.$refs.timeline as any).tail().id
}).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
- this.notes = this.notes.concat(notes);
});
},
- onScroll() {
- const current = window.scrollY + window.innerHeight;
- if (current > document.body.offsetHeight - 16/*遊び*/) {
- this.more();
- }
- },
warp(date) {
this.date = date;
this.fetch();
@@ -115,7 +114,7 @@ export default Vue.extend({
font-size 18px
color #555
- &:not([data-is-active])
+ &:not([data-active])
color $theme-color
cursor pointer
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 93d17b58fe..898b6b2179 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -8,9 +8,7 @@
<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">
- <router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="user | userPage" v-user-preview="user.id">
- <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
- </router-link>
+ <mk-avatar class="avatar" v-for="user in users" :key="user.id" :user="user"/>
</div>
</div>
<div>
@@ -125,7 +123,8 @@ export default Vue.extend({
flex 1
$width = 1000px
- background-image url('/assets/welcome-bg.svg')
+ background linear-gradient(to bottom, #1e1d65, #bd6659)
+ //background-image url('/assets/welcome-bg.svg')
background-size cover
background-position top center
@@ -216,13 +215,9 @@ export default Vue.extend({
> *
display inline-block
margin 4px
-
- > *
- display inline-block
- width 38px
- height 38px
- vertical-align top
- border-radius 6px
+ width 38px
+ height 38px
+ border-radius 6px
> div:last-child
@@ -230,14 +225,14 @@ export default Vue.extend({
width 410px
background #fff
border-radius 8px
- box-shadow 0 0 0 12px rgba(0, 0, 0, 0.1)
+ 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(0, 0, 0, 0.1)
+ box-shadow 0 1px 0px rgba(#000, 0.1)
> div
position absolute
@@ -309,9 +304,3 @@ export default Vue.extend({
a
color #666
</style>
-
-<style lang="stylus">
-html
-body
- background linear-gradient(to bottom, #1e1d65, #bd6659)
-</style>
diff --git a/src/client/app/desktop/views/widgets/activity.vue b/src/client/app/desktop/views/widgets/activity.vue
index 0bdf4622af..1be87f590c 100644
--- a/src/client/app/desktop/views/widgets/activity.vue
+++ b/src/client/app/desktop/views/widgets/activity.vue
@@ -22,9 +22,11 @@ export default define({
} else {
this.props.design++;
}
+ this.save();
},
viewChanged(view) {
this.props.view = view;
+ this.save();
}
}
});
diff --git a/src/client/app/desktop/views/widgets/channel.vue b/src/client/app/desktop/views/widgets/channel.vue
index 7e96f8ee3d..d21aed40fd 100644
--- a/src/client/app/desktop/views/widgets/channel.vue
+++ b/src/client/app/desktop/views/widgets/channel.vue
@@ -37,6 +37,7 @@ export default define({
methods: {
func() {
this.props.compact = !this.props.compact;
+ this.save();
},
settings() {
const id = window.prompt('チャンネルID');
@@ -61,7 +62,7 @@ export default define({
<style lang="stylus" scoped>
.mkw-channel
background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
overflow hidden
@@ -73,7 +74,7 @@ export default define({
font-size 0.9em
font-weight bold
color #888
- box-shadow 0 1px rgba(0, 0, 0, 0.07)
+ box-shadow 0 1px rgba(#000, 0.07)
> [data-fa]
margin-right 4px
diff --git a/src/client/app/desktop/views/widgets/messaging.vue b/src/client/app/desktop/views/widgets/messaging.vue
index 0f197fb2d7..791d2ff1bb 100644
--- a/src/client/app/desktop/views/widgets/messaging.vue
+++ b/src/client/app/desktop/views/widgets/messaging.vue
@@ -1,13 +1,18 @@
<template>
<div class="mkw-messaging">
- <p class="title" v-if="props.design == 0">%fa:comments%%i18n:@title%</p>
- <mk-messaging ref="index" compact @navigate="navigate"/>
+ <mk-widget-container :show-header="props.design == 0">
+ <template slot="header">%fa:comments%%i18n:@title%</template>
+ <button slot="func" @click="add">%fa:plus%</button>
+
+ <mk-messaging ref="index" compact @navigate="navigate"/>
+ </mk-widget-container>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
import MkMessagingRoomWindow from '../components/messaging-room-window.vue';
+import MkMessagingWindow from '../components/messaging-window.vue';
export default define({
name: 'messaging',
@@ -21,12 +26,16 @@ export default define({
user: user
});
},
+ add() {
+ (this as any).os.new(MkMessagingWindow);
+ },
func() {
if (this.props.design == 1) {
this.props.design = 0;
} else {
this.props.design++;
}
+ this.save();
}
}
});
@@ -34,25 +43,7 @@ export default define({
<style lang="stylus" scoped>
.mkw-messaging
- overflow hidden
- background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
- border-radius 6px
-
- > .title
- z-index 2
- margin 0
- padding 0 16px
- line-height 42px
- font-size 0.9em
- font-weight bold
- color #888
- box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
- > [data-fa]
- margin-right 4px
-
- > .mk-messaging
+ .mk-messaging
max-height 250px
overflow auto
diff --git a/src/client/app/desktop/views/widgets/notifications.vue b/src/client/app/desktop/views/widgets/notifications.vue
index 0c2fa0434d..f75a091480 100644
--- a/src/client/app/desktop/views/widgets/notifications.vue
+++ b/src/client/app/desktop/views/widgets/notifications.vue
@@ -1,10 +1,11 @@
<template>
<div class="mkw-notifications">
- <template v-if="!props.compact">
- <p class="title">%fa:R bell%%i18n:@title%</p>
- <button @click="settings" title="%i18n:@settings%">%fa:cog%</button>
- </template>
- <mk-notifications/>
+ <mk-widget-container :show-header="!props.compact">
+ <template slot="header">%fa:R bell%%i18n:@title%</template>
+ <button slot="func" title="%i18n:@settings%" @click="settings">%fa:cog%</button>
+
+ <mk-notifications :class="$style.notifications"/>
+ </mk-widget-container>
</div>
</template>
@@ -22,49 +23,15 @@ export default define({
},
func() {
this.props.compact = !this.props.compact;
+ this.save();
}
}
});
</script>
-<style lang="stylus" scoped>
-.mkw-notifications
- background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
- border-radius 6px
-
- > .title
- z-index 1
- margin 0
- padding 0 16px
- line-height 42px
- font-size 0.9em
- font-weight bold
- color #888
- box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
- > [data-fa]
- margin-right 4px
-
- > button
- position absolute
- z-index 2
- top 0
- right 0
- padding 0
- width 42px
- font-size 0.9em
- line-height 42px
- color #ccc
-
- &:hover
- color #aaa
-
- &:active
- color #999
-
- > .mk-notifications
- max-height 300px
- overflow auto
+<style lang="stylus" module>
+.notifications
+ max-height 300px
+ overflow auto
</style>
diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
index 6cb1192c24..36fcc20636 100644
--- a/src/client/app/desktop/views/widgets/polls.vue
+++ b/src/client/app/desktop/views/widgets/polls.vue
@@ -1,16 +1,19 @@
<template>
<div class="mkw-polls">
- <template v-if="!props.compact">
- <p class="title">%fa:chart-pie%%i18n:@title%</p>
- <button @click="fetch" title="%i18n:@refresh%">%fa:sync%</button>
- </template>
- <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>
- <mk-poll :note="poll"/>
- </div>
- <p class="empty" v-if="!fetching && poll == null">%i18n:@nothing%</p>
- <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+ <mk-widget-container :show-header="!props.compact">
+ <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="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>
+ <mk-poll :note="poll"/>
+ </div>
+ <p class="empty" v-if="!fetching && poll == null">%i18n:@nothing%</p>
+ <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+ </div>
+ </mk-widget-container>
</div>
</template>
@@ -36,6 +39,7 @@ export default define({
methods: {
func() {
this.props.compact = !this.props.compact;
+ this.save();
},
fetch() {
this.fetching = true;
@@ -60,44 +64,11 @@ export default define({
</script>
<style lang="stylus" scoped>
-.mkw-polls
- background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
- border-radius 6px
-
- > .title
- margin 0
- padding 0 16px
- line-height 42px
- font-size 0.9em
- font-weight bold
- color #888
- border-bottom solid 1px #eee
-
- > [data-fa]
- margin-right 4px
-
- > button
- position absolute
- z-index 2
- top 0
- right 0
- padding 0
- width 42px
- font-size 0.9em
- line-height 42px
- color #ccc
-
- &:hover
- color #aaa
-
- &:active
- color #999
-
+root(isDark)
> .poll
padding 16px
font-size 12px
- color #555
+ color isDark ? #9ea4ad : #555
> p
margin 0 0 8px 0
@@ -120,4 +91,10 @@ export default define({
> [data-fa]
margin-right 4px
+.mkw-polls--body[data-darkmode]
+ root(true)
+
+.mkw-polls--body:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/desktop/views/widgets/post-form.vue b/src/client/app/desktop/views/widgets/post-form.vue
index 627943588f..69b21ad37a 100644
--- a/src/client/app/desktop/views/widgets/post-form.vue
+++ b/src/client/app/desktop/views/widgets/post-form.vue
@@ -29,6 +29,7 @@ export default define({
} else {
this.props.design++;
}
+ this.save();
},
onKeydown(e) {
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey)) this.post();
@@ -59,7 +60,7 @@ export default define({
.mkw-post-form
background #fff
overflow hidden
- border solid 1px rgba(0, 0, 0, 0.075)
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
> .title
@@ -70,7 +71,7 @@ export default define({
font-size 0.9em
font-weight bold
color #888
- box-shadow 0 1px rgba(0, 0, 0, 0.07)
+ box-shadow 0 1px rgba(#000, 0.07)
> [data-fa]
margin-right 4px
diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
index 1b4b11de3c..3b01ed034d 100644
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ b/src/client/app/desktop/views/widgets/profile.vue
@@ -8,12 +8,9 @@
title="クリックでバナー編集"
@click="os.apis.updateBanner"
></div>
- <img class="avatar"
- :src="`${os.i.avatarUrl}?thumbnail&size=96`"
+ <mk-avatar class="avatar" :user="os.i"
@click="os.apis.updateAvatar"
- alt="avatar"
title="クリックでアバター編集"
- v-user-preview="os.i.id"
/>
<router-link class="name" :to="os.i | userPage">{{ os.i | userName }}</router-link>
<p class="username">@{{ os.i | acct }}</p>
@@ -36,16 +33,17 @@ export default define({
} else {
this.props.design++;
}
+ this.save();
}
}
});
</script>
<style lang="stylus" scoped>
-.mkw-profile
+root(isDark)
overflow hidden
- background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
+ background isDark ? #282c37 : #fff
+ border solid 1px rgba(#000, 0.075)
border-radius 6px
&[data-compact]
@@ -54,14 +52,14 @@ export default define({
display block
width 100%
height 100%
- background rgba(0, 0, 0, 0.5)
+ background rgba(#000, 0.5)
> .avatar
top ((100px - 58px) / 2)
left ((100px - 58px) / 2)
border none
border-radius 100%
- box-shadow 0 0 16px rgba(0, 0, 0, 0.5)
+ box-shadow 0 0 16px rgba(#000, 0.5)
> .name
position absolute
@@ -70,7 +68,7 @@ export default define({
margin 0
line-height 100px
color #fff
- text-shadow 0 0 8px rgba(0, 0, 0, 0.5)
+ text-shadow 0 0 8px rgba(#000, 0.5)
> .username
display none
@@ -91,7 +89,7 @@ export default define({
> .banner
height 100px
- background-color #f5f5f5
+ background-color isDark ? #303e4a : #f5f5f5
background-size cover
background-position center
cursor pointer
@@ -103,10 +101,8 @@ export default define({
left 16px
width 58px
height 58px
- margin 0
- border solid 3px #fff
+ border solid 3px isDark ? #282c37 : #fff
border-radius 8px
- vertical-align bottom
cursor pointer
> .name
@@ -114,13 +110,19 @@ export default define({
margin 10px 0 0 84px
line-height 16px
font-weight bold
- color #555
+ color isDark ? #fff : #555
> .username
display block
margin 4px 0 8px 84px
line-height 16px
font-size 0.9em
- color #999
+ color isDark ? #606984 : #999
+
+.mkw-profile[data-darkmode]
+ root(true)
+
+.mkw-profile:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/desktop/views/widgets/timemachine.vue b/src/client/app/desktop/views/widgets/timemachine.vue
index 6db3b14c62..22a4120403 100644
--- a/src/client/app/desktop/views/widgets/timemachine.vue
+++ b/src/client/app/desktop/views/widgets/timemachine.vue
@@ -22,6 +22,7 @@ export default define({
} else {
this.props.design++;
}
+ this.save();
}
}
});
diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue
index fccda3f9d0..c33bf2f2f2 100644
--- a/src/client/app/desktop/views/widgets/trends.vue
+++ b/src/client/app/desktop/views/widgets/trends.vue
@@ -1,15 +1,18 @@
<template>
<div class="mkw-trends">
- <template v-if="!props.compact">
- <p class="title">%fa:fire%%i18n:@title%</p>
- <button @click="fetch" title="%i18n:@refresh%">%fa:sync%</button>
- </template>
- <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
- <div class="note" v-else-if="note != null">
- <p class="text"><router-link :to="note | notePage">{{ note.text }}</router-link></p>
- <p class="author">―<router-link :to="note.user | userPage">@{{ note.user | acct }}</router-link></p>
- </div>
- <p class="empty" v-else>%i18n:@nothing%</p>
+ <mk-widget-container :show-header="!props.compact">
+ <template slot="header">%fa:fire%%i18n:@title%</template>
+ <button slot="func" title="%i18n:@refresh%" @click="fetch">%fa:sync%</button>
+
+ <div class="mkw-trends--body">
+ <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+ <div class="note" v-else-if="note != null">
+ <p class="text"><router-link :to="note | notePage">{{ note.text }}</router-link></p>
+ <p class="author">―<router-link :to="note.user | userPage">@{{ note.user | acct }}</router-link></p>
+ </div>
+ <p class="empty" v-else>%i18n:@nothing%</p>
+ </div>
+ </mk-widget-container>
</div>
</template>
@@ -35,6 +38,7 @@ export default define({
methods: {
func() {
this.props.compact = !this.props.compact;
+ this.save();
},
fetch() {
this.fetching = true;
@@ -63,67 +67,41 @@ export default define({
</script>
<style lang="stylus" scoped>
-.mkw-trends
- background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
- border-radius 6px
-
- > .title
- margin 0
- padding 0 16px
- line-height 42px
- font-size 0.9em
- font-weight bold
- color #888
- border-bottom solid 1px #eee
+root(isDark)
+ .mkw-trends--body
+ > .note
+ padding 16px
+ font-size 12px
+ font-style oblique
+ color #555
- > [data-fa]
- margin-right 4px
+ > p
+ margin 0
- > button
- position absolute
- z-index 2
- top 0
- right 0
- padding 0
- width 42px
- font-size 0.9em
- line-height 42px
- color #ccc
+ > .text,
+ > .author
+ > a
+ color inherit
- &:hover
+ > .empty
+ margin 0
+ padding 16px
+ text-align center
color #aaa
- &:active
- color #999
-
- > .note
- padding 16px
- font-size 12px
- font-style oblique
- color #555
-
- > p
+ > .fetching
margin 0
+ padding 16px
+ text-align center
+ color #aaa
- > .text,
- > .author
- > a
- color inherit
-
- > .empty
- margin 0
- padding 16px
- text-align center
- color #aaa
+ > [data-fa]
+ margin-right 4px
- > .fetching
- margin 0
- padding 16px
- text-align center
- color #aaa
+.mkw-trends[data-darkmode]
+ root(true)
- > [data-fa]
- margin-right 4px
+.mkw-trends:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue
index 0955ebbd71..328fa56697 100644
--- a/src/client/app/desktop/views/widgets/users.vue
+++ b/src/client/app/desktop/views/widgets/users.vue
@@ -1,23 +1,24 @@
<template>
<div class="mkw-users">
- <template v-if="!props.compact">
- <p class="title">%fa:users%%i18n:@title%</p>
- <button @click="refresh" title="%i18n:@refresh%">%fa:sync%</button>
- </template>
- <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
- <template v-else-if="users.length != 0">
- <div class="user" v-for="_user in users">
- <router-link class="avatar-anchor" :to="_user | userPage">
- <img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
- </router-link>
- <div class="body">
- <router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link>
- <p class="username">@{{ _user | acct }}</p>
- </div>
- <mk-follow-button :user="_user"/>
+ <mk-widget-container :show-header="!props.compact">
+ <template slot="header">%fa:users%%i18n:@title%</template>
+ <button slot="func" title="%i18n:@refresh%" @click="refresh">%fa:sync%</button>
+
+ <div class="mkw-users--body">
+ <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+ <template v-else-if="users.length != 0">
+ <div class="user" v-for="_user in users">
+ <mk-avatar class="avatar" :user="_user"/>
+ <div class="body">
+ <router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link>
+ <p class="username">@{{ _user | acct }}</p>
+ </div>
+ <mk-follow-button :user="_user"/>
+ </div>
+ </template>
+ <p class="empty" v-else>%i18n:@no-one%</p>
</div>
- </template>
- <p class="empty" v-else>%i18n:@no-one%</p>
+ </mk-widget-container>
</div>
</template>
@@ -45,6 +46,7 @@ export default define({
methods: {
func() {
this.props.compact = !this.props.compact;
+ this.save();
},
fetch() {
this.fetching = true;
@@ -71,100 +73,69 @@ export default define({
</script>
<style lang="stylus" scoped>
-.mkw-users
- background #fff
- border solid 1px rgba(0, 0, 0, 0.075)
- border-radius 6px
-
- > .title
- margin 0
- padding 0 16px
- line-height 42px
- font-size 0.9em
- font-weight bold
- color #888
- border-bottom solid 1px #eee
-
- > [data-fa]
- margin-right 4px
-
- > button
- position absolute
- z-index 2
- top 0
- right 0
- padding 0
- width 42px
- font-size 0.9em
- line-height 42px
- color #ccc
-
- &:hover
- color #aaa
-
- &:active
- color #999
-
- > .user
- padding 16px
- border-bottom solid 1px #eee
+root(isDark)
+ .mkw-users--body
+ > .user
+ padding 16px
+ border-bottom solid 1px isDark ? #1c2023 : #eee
- &:last-child
- border-bottom none
+ &:last-child
+ border-bottom none
- &:after
- content ""
- display block
- clear both
-
- > .avatar-anchor
- display block
- float left
- margin 0 12px 0 0
+ &:after
+ content ""
+ display block
+ clear both
> .avatar
display block
+ float left
+ margin 0 12px 0 0
width 42px
height 42px
- margin 0
border-radius 8px
- vertical-align bottom
- > .body
- float left
- width calc(100% - 54px)
+ > .body
+ float left
+ width calc(100% - 54px)
- > .name
- margin 0
- font-size 16px
- line-height 24px
- color #555
+ > .name
+ margin 0
+ font-size 16px
+ line-height 24px
+ color isDark ? #fff : #555
- > .username
- display block
- margin 0
- font-size 15px
- line-height 16px
- color #ccc
+ > .username
+ display block
+ margin 0
+ font-size 15px
+ line-height 16px
+ color isDark ? #606984 : #ccc
+
+ > .mk-follow-button
+ position absolute
+ top 16px
+ right 16px
- > .mk-follow-button
- position absolute
- top 16px
- right 16px
+ > .empty
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > .fetching
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
- > .empty
- margin 0
- padding 16px
- text-align center
- color #aaa
+ > [data-fa]
+ margin-right 4px
- > .fetching
- margin 0
- padding 16px
- text-align center
- color #aaa
+.mkw-users[data-darkmode]
+ root(true)
- > [data-fa]
- margin-right 4px
+.mkw-users:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/init.css b/src/client/app/init.css
index 2587f63943..fa59195f71 100644
--- a/src/client/app/init.css
+++ b/src/client/app/init.css
@@ -56,6 +56,13 @@ body > noscript {
animation-delay: 0.32s;
}
+html[data-darkmode] #ini {
+ background: #191b22;
+}
+ html[data-darkmode] #ini > p {
+ color: #fff;
+ }
+
@keyframes ini {
0%, 80%, 100% {
opacity: 1;
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
index 990933ec0e..4908b73b23 100644
--- a/src/client/app/init.ts
+++ b/src/client/app/init.ts
@@ -3,6 +3,7 @@
*/
import Vue from 'vue';
+import Vuex from 'vuex';
import VueRouter from 'vue-router';
import VModal from 'vue-js-modal';
import * as TreeView from 'vue-json-tree-view';
@@ -13,7 +14,7 @@ import ElementLocaleJa from 'element-ui/lib/locale/lang/ja';
import App from './app.vue';
import checkForUpdate from './common/scripts/check-for-update';
-import MiOS, { API } from './common/mios';
+import MiOS, { API } from './mios';
import { version, codename, lang } from './config';
let elementLocale;
@@ -23,6 +24,7 @@ switch (lang) {
default: elementLocale = ElementLocaleEn; break;
}
+Vue.use(Vuex);
Vue.use(VueRouter);
Vue.use(VModal);
Vue.use(TreeView);
@@ -47,6 +49,48 @@ 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!
*/
@@ -102,21 +146,15 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
return {
os,
api: os.api,
- apis: os.apis
+ apis: os.apis,
+ clientSettings: os.store.state.settings.data
};
}
});
const app = new Vue({
+ store: os.store,
router,
- created() {
- this.$watch('os.i', i => {
- // キャッシュ更新
- localStorage.setItem('me', JSON.stringify(i));
- }, {
- deep: true
- });
- },
render: createEl => createEl(App)
});
diff --git a/src/client/app/common/mios.ts b/src/client/app/mios.ts
index 6d6d6b3e68..2373b0d8d2 100644
--- a/src/client/app/common/mios.ts
+++ b/src/client/app/mios.ts
@@ -3,18 +3,19 @@ import { EventEmitter } from 'eventemitter3';
import * as merge from 'object-assign-deep';
import * as uuid from 'uuid';
-import { hostname, apiUrl, swPublickey, version, lang, googleMapsApiKey } from '../config';
-import Progress from './scripts/loading';
-import Connection from './scripts/streaming/stream';
-import { HomeStreamManager } from './scripts/streaming/home';
-import { DriveStreamManager } from './scripts/streaming/drive';
-import { ServerStreamManager } from './scripts/streaming/server';
-import { MessagingIndexStreamManager } from './scripts/streaming/messaging-index';
-import { OthelloStreamManager } from './scripts/streaming/othello';
+import initStore from './store';
+import { hostname, apiUrl, swPublickey, version, lang, googleMapsApiKey } from './config';
+import Progress from './common/scripts/loading';
+import Connection from './common/scripts/streaming/stream';
+import { HomeStreamManager } from './common/scripts/streaming/home';
+import { DriveStreamManager } from './common/scripts/streaming/drive';
+import { ServerStreamManager } from './common/scripts/streaming/server';
+import { MessagingIndexStreamManager } from './common/scripts/streaming/messaging-index';
+import { OthelloStreamManager } from './common/scripts/streaming/othello';
-import Err from '../common/views/components/connect-failed.vue';
-import { LocalTimelineStreamManager } from './scripts/streaming/local-timeline';
-import { GlobalTimelineStreamManager } from './scripts/streaming/global-timeline';
+import Err from './common/views/components/connect-failed.vue';
+import { LocalTimelineStreamManager } from './common/scripts/streaming/local-timeline';
+import { GlobalTimelineStreamManager } from './common/scripts/streaming/global-timeline';
//#region api requests
let spinner = null;
@@ -78,6 +79,7 @@ export default class MiOS extends EventEmitter {
propsData: props
}).$mount();
document.body.appendChild(w.$el);
+ return w;
}
/**
@@ -106,6 +108,8 @@ export default class MiOS extends EventEmitter {
return localStorage.getItem('enableSounds') == 'true';
}
+ public store: ReturnType<typeof initStore>;
+
public apis: API;
/**
@@ -221,8 +225,14 @@ export default class MiOS extends EventEmitter {
console.error.apply(null, args);
}
+ public bakeMe() {
+ // ローカルストレージにキャッシュ
+ localStorage.setItem('me', JSON.stringify(this.i));
+ }
+
public signout() {
localStorage.removeItem('me');
+ localStorage.removeItem('settings');
document.cookie = `i=; domain=${hostname}; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
location.href = '/';
}
@@ -232,6 +242,8 @@ export default class MiOS extends EventEmitter {
* @param callback A function that call when initialized
*/
public async init(callback) {
+ this.store = initStore(this);
+
//#region Init stream managers
this.streams.serverStream = new ServerStreamManager(this);
@@ -296,21 +308,11 @@ export default class MiOS extends EventEmitter {
// フェッチが完了したとき
const fetched = me => {
- if (me) {
- // デフォルトの設定をマージ
- me.clientSettings = Object.assign({
- fetchOnScroll: true,
- showMaps: true,
- showPostFormOnTopOfTl: false,
- gradientWindowHeader: false
- }, me.clientSettings);
-
- // ローカルストレージにキャッシュ
- localStorage.setItem('me', JSON.stringify(me));
- }
-
this.i = me;
+ // ローカルストレージにキャッシュ
+ this.bakeMe();
+
this.emit('signedin');
// Finish init
@@ -327,6 +329,14 @@ export default class MiOS extends EventEmitter {
// Get cached account data
const cachedMe = JSON.parse(localStorage.getItem('me'));
+ //#region キャッシュされた設定を復元
+ const cachedSettings = JSON.parse(localStorage.getItem('settings'));
+
+ if (cachedSettings) {
+ this.store.dispatch('settings/merge', cachedSettings);
+ }
+ //#endregion
+
// キャッシュがあったとき
if (cachedMe) {
if (cachedMe.token == null) {
@@ -340,12 +350,23 @@ export default class MiOS extends EventEmitter {
// 後から新鮮なデータをフェッチ
fetchme(cachedMe.token, freshData => {
merge(cachedMe, freshData);
+
+ this.store.dispatch('settings/merge', freshData.clientSettings);
});
} else {
// Get token from cookie
const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1];
- fetchme(i, fetched);
+ fetchme(i, me => {
+ if (me) {
+ this.store.dispatch('settings/merge', me.clientSettings);
+
+ fetched(me);
+ } else {
+ // Finish init
+ callback();
+ }
+ });
}
}
@@ -450,7 +471,7 @@ export default class MiOS extends EventEmitter {
};
const promise = new Promise((resolve, reject) => {
- const viaStream = this.stream.hasConnection &&
+ const viaStream = this.stream && this.stream.hasConnection &&
(localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true);
if (viaStream) {
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index 1de4891973..2e9805e0d0 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -55,15 +55,15 @@ init((launch) => {
{ path: '/signup', name: 'signup', component: MkSignup },
{ path: '/i/settings', component: MkSettings },
{ path: '/i/settings/profile', component: MkProfileSetting },
- { path: '/i/notifications', component: MkNotifications },
- { path: '/i/messaging', component: MkMessaging },
+ { path: '/i/notifications', name: 'notifications', component: MkNotifications },
+ { path: '/i/messaging', name: 'messaging', component: MkMessaging },
{ path: '/i/messaging/:user', component: MkMessagingRoom },
- { path: '/i/drive', component: MkDrive },
+ { path: '/i/drive', name: 'drive', component: MkDrive },
{ path: '/i/drive/folder/:folder', component: MkDrive },
{ path: '/i/drive/file/:file', component: MkDrive },
{ path: '/selectdrive', component: MkSelectDrive },
{ path: '/search', component: MkSearch },
- { path: '/othello', component: MkOthello },
+ { path: '/othello', name: 'othello', component: MkOthello },
{ path: '/othello/:game', component: MkOthello },
{ path: '/@:user', component: MkUser },
{ path: '/@:user/followers', component: MkFollowers },
diff --git a/src/client/app/mobile/style.styl b/src/client/app/mobile/style.styl
index 81912a2483..847ae8eec5 100644
--- a/src/client/app/mobile/style.styl
+++ b/src/client/app/mobile/style.styl
@@ -8,6 +8,10 @@
html
height 100%
+ background #ececed
+
+ &[data-darkmode]
+ background #191B22
body
display flex
diff --git a/src/client/app/mobile/views/components/drive-file-chooser.vue b/src/client/app/mobile/views/components/drive-file-chooser.vue
index 41536afbd4..d95d5fa223 100644
--- a/src/client/app/mobile/views/components/drive-file-chooser.vue
+++ b/src/client/app/mobile/views/components/drive-file-chooser.vue
@@ -54,7 +54,7 @@ export default Vue.extend({
width 100%
height 100%
padding 8px
- background rgba(0, 0, 0, 0.2)
+ background rgba(#000, 0.2)
> .body
width 100%
diff --git a/src/client/app/mobile/views/components/drive-folder-chooser.vue b/src/client/app/mobile/views/components/drive-folder-chooser.vue
index bfd8fbda6f..7934fb7816 100644
--- a/src/client/app/mobile/views/components/drive-folder-chooser.vue
+++ b/src/client/app/mobile/views/components/drive-folder-chooser.vue
@@ -38,7 +38,7 @@ export default Vue.extend({
width 100%
height 100%
padding 8px
- background rgba(0, 0, 0, 0.2)
+ background rgba(#000, 0.2)
> .body
width 100%
diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue
index c7be7d1879..764822e98c 100644
--- a/src/client/app/mobile/views/components/drive.file-detail.vue
+++ b/src/client/app/mobile/views/components/drive.file-detail.vue
@@ -139,7 +139,7 @@ export default Vue.extend({
max-width 100%
max-height 300px
margin 0 auto
- box-shadow 1px 1px 4px rgba(0, 0, 0, 0.2)
+ box-shadow 1px 1px 4px rgba(#000, 0.2)
> footer
padding 8px 8px 0 8px
@@ -226,7 +226,7 @@ export default Vue.extend({
background-color #767676
background-image none
border-color #444
- box-shadow 0 1px 3px rgba(0, 0, 0, 0.075), inset 0 0 5px rgba(0, 0, 0, 0.2)
+ box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2)
> [data-fa]
margin-right 4px
diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue
index 7aa666e1bb..ef3432a3ec 100644
--- a/src/client/app/mobile/views/components/drive.vue
+++ b/src/client/app/mobile/views/components/drive.vue
@@ -474,11 +474,11 @@ export default Vue.extend({
overflow auto
white-space nowrap
font-size 0.9em
- color rgba(0, 0, 0, 0.67)
+ color rgba(#000, 0.67)
-webkit-backdrop-filter blur(12px)
backdrop-filter blur(12px)
background-color rgba(#fff, 0.75)
- border-bottom solid 1px rgba(0, 0, 0, 0.13)
+ border-bottom solid 1px rgba(#000, 0.13)
> p
> a
@@ -555,7 +555,7 @@ export default Vue.extend({
display inline-block
position absolute
top 0
- background rgba(0, 0, 0, 0.2)
+ background rgba(#000, 0.2)
border-radius 100%
animation sk-bounce 2.0s infinite ease-in-out
diff --git a/src/client/app/mobile/views/components/friends-maker.vue b/src/client/app/mobile/views/components/friends-maker.vue
index 961a5f568a..ba4abe341f 100644
--- a/src/client/app/mobile/views/components/friends-maker.vue
+++ b/src/client/app/mobile/views/components/friends-maker.vue
@@ -57,7 +57,7 @@ export default Vue.extend({
.mk-friends-maker
background #fff
border-radius 8px
- box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+ box-shadow 0 0 0 1px rgba(#000, 0.2)
> .title
margin 0
diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts
index 9346700304..5ed8427b05 100644
--- a/src/client/app/mobile/views/components/index.ts
+++ b/src/client/app/mobile/views/components/index.ts
@@ -1,7 +1,6 @@
import Vue from 'vue';
import ui from './ui.vue';
-import timeline from './timeline.vue';
import note from './note.vue';
import notes from './notes.vue';
import mediaImage from './media-image.vue';
@@ -20,11 +19,11 @@ import notificationPreview from './notification-preview.vue';
import usersList from './users-list.vue';
import userPreview from './user-preview.vue';
import userTimeline from './user-timeline.vue';
+import userListTimeline from './user-list-timeline.vue';
import activity from './activity.vue';
import widgetContainer from './widget-container.vue';
Vue.component('mk-ui', ui);
-Vue.component('mk-timeline', timeline);
Vue.component('mk-note', note);
Vue.component('mk-notes', notes);
Vue.component('mk-media-image', mediaImage);
@@ -43,5 +42,6 @@ Vue.component('mk-notification-preview', notificationPreview);
Vue.component('mk-users-list', usersList);
Vue.component('mk-user-preview', userPreview);
Vue.component('mk-user-timeline', userTimeline);
+Vue.component('mk-user-list-timeline', userListTimeline);
Vue.component('mk-activity', activity);
Vue.component('mk-widget-container', widgetContainer);
diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue
index cfc2134988..92d1cdc6f5 100644
--- a/src/client/app/mobile/views/components/media-image.vue
+++ b/src/client/app/mobile/views/components/media-image.vue
@@ -6,12 +6,20 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['image'],
+ props: {
+ image: {
+ type: Object,
+ required: true
+ },
+ raw: {
+ default: false
+ }
+ },
computed: {
style(): any {
return {
'background-color': this.image.properties.avgColor ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
- 'background-image': `url(${this.image.url}?thumbnail&size=512)`
+ 'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
};
}
}
diff --git a/src/client/app/mobile/views/components/note-card.vue b/src/client/app/mobile/views/components/note-card.vue
index 393fa9b831..89700b5e82 100644
--- a/src/client/app/mobile/views/components/note-card.vue
+++ b/src/client/app/mobile/views/components/note-card.vue
@@ -27,17 +27,17 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-note-card
+root(isDark)
display inline-block
width 150px
//height 120px
font-size 12px
- background #fff
+ background isDark ? #282c37 : #fff
border-radius 4px
> a
display block
- color #2c3940
+ color isDark ? #fff : #2c3940
&:hover
text-decoration none
@@ -75,11 +75,17 @@ export default Vue.extend({
left 0
width 100%
height 20px
- background linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #fff 100%)
+ background isDark ? linear-gradient(to bottom, rgba(#282c37, 0) 0%, #282c37 100%) : linear-gradient(to bottom, rgba(#fff, 0) 0%, #fff 100%)
> .mk-time
display inline-block
padding 8px
color #aaa
+.mk-note-card[data-darkmode]
+ root(true)
+
+.mk-note-card:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/mobile/views/components/note-detail.sub.vue b/src/client/app/mobile/views/components/note-detail.sub.vue
index 06f442d308..e515fda8a6 100644
--- a/src/client/app/mobile/views/components/note-detail.sub.vue
+++ b/src/client/app/mobile/views/components/note-detail.sub.vue
@@ -1,8 +1,6 @@
<template>
<div class="root sub">
- <router-link class="avatar-anchor" :to="note.user | userPage">
- <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
- </router-link>
+ <mk-avatar class="avatar" :user="note.user"/>
<div class="main">
<header>
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
@@ -27,35 +25,29 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.root.sub
+root(isDark)
padding 8px
font-size 0.9em
- background #fdfdfd
+ background isDark ? #21242d : #fdfdfd
@media (min-width 500px)
padding 12px
+ @media (min-width 600px)
+ padding 24px 32px
+
&:after
content ""
display block
clear both
- &:hover
- > .main > footer > button
- color #888
-
- > .avatar-anchor
+ > .avatar
display block
float left
margin 0 12px 0 0
-
- > .avatar
- display block
- width 48px
- height 48px
- margin 0
- border-radius 8px
- vertical-align bottom
+ width 48px
+ height 48px
+ border-radius 8px
> .main
float left
@@ -63,6 +55,7 @@ export default Vue.extend({
> header
display flex
+ align-items baseline
margin-bottom 4px
white-space nowrap
@@ -71,7 +64,7 @@ export default Vue.extend({
margin 0 .5em 0 0
padding 0
overflow hidden
- color #607073
+ color isDark ? #fff : #607073
font-size 1em
font-weight 700
text-align left
@@ -84,11 +77,11 @@ export default Vue.extend({
> .username
text-align left
margin 0 .5em 0 0
- color #d1d8da
+ color isDark ? #606984 : #d1d8da
> .time
margin-left auto
- color #b2b8bb
+ color isDark ? #606984 : #b2b8bb
> .body
@@ -97,7 +90,12 @@ export default Vue.extend({
margin 0
padding 0
font-size 1.1em
- color #717171
+ color isDark ? #959ba7 : #717171
-</style>
+.root.sub[data-darkmode]
+ root(true)
+
+.root.sub:not([data-darkmode])
+ root(false)
+</style>
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index 7d2747751e..5a7226faac 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -17,29 +17,27 @@
</div>
<div class="renote" v-if="isRenote">
<p>
- <router-link class="avatar-anchor" :to="note.user | userPage">
- <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
- </router-link>
- %fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote
+ <mk-avatar class="avatar" :user="note.user"/>%fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote
</p>
</div>
<article>
<header>
- <router-link class="avatar-anchor" :to="p.user | userPage">
- <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
- </router-link>
+ <mk-avatar class="avatar" :user="p.user"/>
<div>
<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
<span class="username">@{{ p.user | acct }}</span>
</div>
</header>
<div class="body">
- <mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
+ <div class="text">
+ <span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
+ <mk-note-html v-if="p.text" :text="p.text" :i="os.i"/>
+ </div>
<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>
<div class="media" v-if="p.media.length > 0">
- <mk-media-list :media-list="p.media"/>
+ <mk-media-list :media-list="p.media" :raw="true"/>
</div>
<mk-poll v-if="p.poll" :note="p"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
@@ -55,7 +53,9 @@
<footer>
<mk-reactions-viewer :note="p"/>
<button @click="reply" title="%i18n:@reply%">
- %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+ <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">
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
@@ -147,7 +147,7 @@ export default Vue.extend({
// Draw map
if (this.p.geo) {
- const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+ const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
if (shouldShowMap) {
(this as any).os.getGoogleMaps().then(maps => {
const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
@@ -207,15 +207,18 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-note-detail
+root(isDark)
overflow hidden
margin 0 auto
padding 0
width 100%
text-align left
- background #fff
+ background isDark ? #282C37 : #fff
border-radius 8px
- box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+ box-shadow 0 0 2px rgba(#000, 0.1)
+
+ @media (min-width 500px)
+ box-shadow 0 8px 32px rgba(#000, 0.1)
> .fetching
padding 64px 0
@@ -229,45 +232,37 @@ export default Vue.extend({
text-align center
color #999
cursor pointer
- background #fafafa
+ background isDark ? #21242d : #fafafa
outline none
border none
- border-bottom solid 1px #eef0f2
+ border-bottom solid 1px isDark ? #1c2023 : #eef0f2
border-radius 6px 6px 0 0
box-shadow none
&:hover
- background #f6f6f6
-
- &:active
- background #f0f0f0
+ background isDark ? #16181d : #f6f6f6
&:disabled
color #ccc
> .context
> *
- border-bottom 1px solid #eef0f2
+ border-bottom 1px solid isDark ? #1c2023 : #eef0f2
> .renote
color #9dbb00
- background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+ background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
> p
margin 0
padding 16px 32px
- .avatar-anchor
+ .avatar
display inline-block
-
- .avatar
- vertical-align bottom
- min-width 28px
- min-height 28px
- max-width 28px
- max-height 28px
- margin 0 8px 0 0
- border-radius 6px
+ width 28px
+ height 28px
+ margin 0 8px 0 0
+ border-radius 6px
[data-fa]
margin-right 4px
@@ -279,7 +274,7 @@ export default Vue.extend({
padding-top 8px
> .reply-to
- border-bottom 1px solid #eef0f2
+ border-bottom 1px solid isDark ? #1c2023 : #eef0f2
> article
padding 14px 16px 9px 16px
@@ -292,36 +287,27 @@ export default Vue.extend({
display block
clear both
- &:hover
- > .main > footer > button
- color #888
-
> header
display flex
- line-height 1.1
+ line-height 1.1em
- > .avatar-anchor
+ > .avatar
display block
- padding 0 .5em 0 0
-
- > .avatar
- display block
- width 54px
- height 54px
- margin 0
- border-radius 8px
- vertical-align bottom
+ margin 0 12px 0 0
+ width 54px
+ height 54px
+ border-radius 8px
- @media (min-width 500px)
- width 60px
- height 60px
+ @media (min-width 500px)
+ width 60px
+ height 60px
> div
> .name
display inline-block
margin .4em 0
- color #777
+ color isDark ? #fff : #627079
font-size 16px
font-weight bold
text-align left
@@ -334,11 +320,22 @@ export default Vue.extend({
display block
text-align left
margin 0
- color #ccc
+ color isDark ? #606984 : #ccc
> .body
padding 8px 0
+ > .text
+ display block
+ margin 0
+ padding 0
+ overflow-wrap break-word
+ font-size 16px
+ color isDark ? #fff : #717171
+
+ @media (min-width 500px)
+ font-size 24px
+
> .renote
margin 8px 0
@@ -394,7 +391,7 @@ export default Vue.extend({
> .time
font-size 16px
- color #c0c0c0
+ color isDark ? #606984 : #c0c0c0
> footer
font-size 1.2em
@@ -406,14 +403,14 @@ export default Vue.extend({
border none
box-shadow none
font-size 1em
- color #ddd
+ color isDark ? #606984 : #ddd
cursor pointer
&:not(:last-child)
margin-right 28px
&:hover
- color #666
+ color isDark ? #9198af : #666
> .count
display inline
@@ -425,20 +422,12 @@ export default Vue.extend({
> .replies
> *
- border-top 1px solid #eef0f2
+ border-top 1px solid isDark ? #1c2023 : #eef0f2
-</style>
+.mk-note-detail[data-darkmode]
+ root(true)
-<style lang="stylus" module>
-.text
- display block
- margin 0
- padding 0
- overflow-wrap break-word
- font-size 16px
- color #717171
-
- @media (min-width 500px)
- font-size 24px
+.mk-note-detail:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
index b9a6db315d..ec11f23315 100644
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -1,8 +1,6 @@
<template>
<div class="mk-note-preview">
- <router-link class="avatar-anchor" :to="note.user | userPage">
- <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
- </router-link>
+ <mk-avatar class="avatar" :user="note.user"/>
<div class="main">
<header>
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
@@ -27,33 +25,23 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-note-preview
+root(isDark)
margin 0
padding 0
font-size 0.9em
- background #fff
&:after
content ""
display block
clear both
- &:hover
- > .main > footer > button
- color #888
-
- > .avatar-anchor
+ > .avatar
display block
float left
margin 0 12px 0 0
-
- > .avatar
- display block
- width 48px
- height 48px
- margin 0
- border-radius 8px
- vertical-align bottom
+ width 48px
+ height 48px
+ border-radius 8px
> .main
float left
@@ -61,6 +49,7 @@ export default Vue.extend({
> header
display flex
+ align-items baseline
margin-bottom 4px
white-space nowrap
@@ -69,7 +58,7 @@ export default Vue.extend({
margin 0 .5em 0 0
padding 0
overflow hidden
- color #607073
+ color isDark ? #fff : #607073
font-size 1em
font-weight 700
text-align left
@@ -82,11 +71,11 @@ export default Vue.extend({
> .username
text-align left
margin 0 .5em 0 0
- color #d1d8da
+ color isDark ? #606984 : #d1d8da
> .time
margin-left auto
- color #b2b8bb
+ color isDark ? #606984 : #b2b8bb
> .body
@@ -95,6 +84,12 @@ export default Vue.extend({
margin 0
padding 0
font-size 1.1em
- color #717171
+ color isDark ? #959ba7 : #717171
+
+.mk-note-preview[data-darkmode]
+ root(true)
+
+.mk-note-preview:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue
index d489f3a053..82025291da 100644
--- a/src/client/app/mobile/views/components/note.sub.vue
+++ b/src/client/app/mobile/views/components/note.sub.vue
@@ -1,15 +1,22 @@
<template>
<div class="sub">
- <router-link class="avatar-anchor" :to="note.user | userPage">
- <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
- </router-link>
+ <mk-avatar class="avatar" :user="note.user"/>
<div class="main">
<header>
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
<span class="username">@{{ note.user | acct }}</span>
- <router-link class="created-at" :to="note | notePage">
- <mk-time :time="note.createdAt"/>
- </router-link>
+ <div class="info">
+ <span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
+ <router-link class="created-at" :to="note | notePage">
+ <mk-time :time="note.createdAt"/>
+ </router-link>
+ <span class="visibility" v-if="note.visibility != 'public'">
+ <template v-if="note.visibility == 'home'">%fa:home%</template>
+ <template v-if="note.visibility == 'followers'">%fa:unlock%</template>
+ <template v-if="note.visibility == 'specified'">%fa:envelope%</template>
+ <template v-if="note.visibility == 'private'">%fa:lock%</template>
+ </span>
+ </div>
</header>
<div class="body">
<mk-sub-note-content class="text" :note="note"/>
@@ -27,34 +34,31 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.sub
- font-size 0.9em
+root(isDark)
padding 16px
+ font-size 0.9em
+ background isDark ? #21242d : #fcfcfc
+
+ @media (min-width 600px)
+ padding 24px 32px
&:after
content ""
display block
clear both
- > .avatar-anchor
+ > .avatar
display block
float left
margin 0 10px 0 0
+ width 44px
+ height 44px
+ border-radius 8px
@media (min-width 500px)
margin-right 16px
-
- > .avatar
- display block
- width 44px
- height 44px
- margin 0
- border-radius 8px
- vertical-align bottom
-
- @media (min-width 500px)
- width 52px
- height 52px
+ width 52px
+ height 52px
> .main
float left
@@ -65,6 +69,7 @@ export default Vue.extend({
> header
display flex
+ align-items baseline
margin-bottom 2px
white-space nowrap
@@ -73,7 +78,7 @@ export default Vue.extend({
margin 0 0.5em 0 0
padding 0
overflow hidden
- color #607073
+ color isDark ? #fff : #607073
font-size 1em
font-weight 700
text-align left
@@ -86,24 +91,40 @@ export default Vue.extend({
> .username
text-align left
margin 0
- color #d1d8da
+ color isDark ? #606984 : #d1d8da
- > .created-at
+ > .info
margin-left auto
- color #b2b8bb
+ font-size 0.9em
+
+ > *
+ color isDark ? #606984 : #b2b8bb
+
+ > .mobile
+ margin-right 6px
+
+ > .visibility
+ margin-left 6px
> .body
+ max-height 128px
+ overflow hidden
> .text
cursor default
margin 0
padding 0
font-size 1.1em
- color #717171
+ color isDark ? #959ba7 : #717171
pre
max-height 120px
font-size 80%
-</style>
+.sub[data-darkmode]
+ root(true)
+.sub:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index cccb8875b4..d66f5a1016 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -1,24 +1,18 @@
<template>
<div class="note" :class="{ renote: isRenote }">
- <div class="reply-to" v-if="p.reply">
+ <div class="reply-to" v-if="p.reply && (!os.isSignedIn || clientSettings.showReplyTarget)">
<x-sub :note="p.reply"/>
</div>
<div class="renote" v-if="isRenote">
- <p>
- <router-link class="avatar-anchor" :to="note.user | userPage">
- <img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
- </router-link>
- %fa:retweet%
- <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>
- </p>
+ <mk-avatar class="avatar" :user="note.user"/>
+ %fa:retweet%
+ <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>
<mk-time :time="note.createdAt"/>
</div>
<article>
- <router-link class="avatar-anchor" :to="p.user | userPage">
- <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
- </router-link>
+ <mk-avatar class="avatar" :user="p.user"/>
<div class="main">
<header>
<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
@@ -29,36 +23,49 @@
<router-link class="created-at" :to="p | notePage">
<mk-time :time="p.createdAt"/>
</router-link>
+ <span class="visibility" v-if="p.visibility != 'public'">
+ <template v-if="p.visibility == 'home'">%fa:home%</template>
+ <template v-if="p.visibility == 'followers'">%fa:unlock%</template>
+ <template v-if="p.visibility == 'specified'">%fa:envelope%</template>
+ <template v-if="p.visibility == 'private'">%fa:lock%</template>
+ </span>
</div>
</header>
<div class="body">
<p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p>
- <div class="text">
- <a class="reply" v-if="p.reply">
- %fa:reply%
- </a>
- <mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
- <a class="rp" v-if="p.renote != null">RP:</a>
+ <p v-if="p.cw != null" class="cw">
+ <span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
+ <span class="toggle" @click="showContent = !showContent">{{ showContent ? '隠す' : 'もっと見る' }}</span>
+ </p>
+ <div class="content" v-show="p.cw == null || showContent">
+ <div class="text">
+ <span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
+ <a class="reply" v-if="p.reply">%fa:reply%</a>
+ <mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
+ <a class="rp" v-if="p.renote != null">RP:</a>
+ </div>
+ <div class="media" v-if="p.media.length > 0">
+ <mk-media-list :media-list="p.media"/>
+ </div>
+ <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
+ <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>
+ <mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+ <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>
+ <div class="map" v-if="p.geo" ref="map"></div>
+ <div class="renote" v-if="p.renote">
+ <mk-note-preview :note="p.renote"/>
+ </div>
</div>
- <div class="media" v-if="p.media.length > 0">
- <mk-media-list :media-list="p.media"/>
- </div>
- <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
- <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>
- <mk-url-preview v-for="url in urls" :url="url" :key="url"/>
- <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>
- <div class="map" v-if="p.geo" ref="map"></div>
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
- <div class="renote" v-if="p.renote">
- <mk-note-preview :note="p.renote"/>
- </div>
</div>
<footer>
<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
<button @click="reply">
- %fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+ <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">
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
@@ -92,6 +99,7 @@ export default Vue.extend({
data() {
return {
+ showContent: false,
connection: null,
connectionId: null
};
@@ -142,7 +150,7 @@ export default Vue.extend({
// Draw map
if (this.p.geo) {
- const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+ const shouldShowMap = (this as any).os.isSignedIn ? (this as any).clientSettings.showMaps : true;
if (shouldShowMap) {
(this as any).os.getGoogleMaps().then(maps => {
const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
@@ -229,15 +237,9 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.note
+root(isDark)
font-size 12px
- border-bottom solid 1px #eaeaea
-
- &:first-child
- border-radius 8px 8px 0 0
-
- > .renote
- border-radius 8px 8px 0 0
+ border-bottom solid 1px isDark ? #1c2023 : #eaeaea
&:last-of-type
border-bottom none
@@ -249,83 +251,78 @@ export default Vue.extend({
font-size 16px
> .renote
+ display flex
+ align-items center
+ padding 8px 16px
+ line-height 28px
color #9dbb00
- background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+ background isDark ? linear-gradient(to bottom, #314027 0%, #282c37 100%) : linear-gradient(to bottom, #edfde2 0%, #fff 100%)
- > p
- margin 0
- padding 8px 16px
- line-height 28px
+ @media (min-width 500px)
+ padding 16px
- @media (min-width 500px)
- padding 16px
+ @media (min-width 600px)
+ padding 16px 32px
+
+ .avatar
+ display inline-block
+ width 28px
+ height 28px
+ margin 0 8px 0 0
+ border-radius 6px
- .avatar-anchor
- display inline-block
+ [data-fa]
+ margin-right 4px
- .avatar
- vertical-align bottom
- width 28px
- height 28px
- margin 0 8px 0 0
- border-radius 6px
+ > span
+ flex-shrink 0
- [data-fa]
- margin-right 4px
+ &:last-of-type
+ margin-right 8px
- .name
- font-weight bold
+ .name
+ overflow hidden
+ flex-shrink 1
+ text-overflow ellipsis
+ white-space nowrap
+ font-weight bold
> .mk-time
- position absolute
- top 8px
- right 16px
+ display block
+ margin-left auto
+ flex-shrink 0
font-size 0.9em
- line-height 28px
-
- @media (min-width 500px)
- top 16px
& + article
padding-top 8px
- > .reply-to
- background rgba(0, 0, 0, 0.0125)
-
- > .mk-note-preview
- background transparent
-
> article
- padding 14px 16px 9px 16px
+ padding 16px 16px 9px
+
+ @media (min-width 600px)
+ padding 32px 32px 22px
&:after
content ""
display block
clear both
- > .avatar-anchor
+ > .avatar
display block
float left
margin 0 10px 8px 0
- position -webkit-sticky
- position sticky
- top 62px
+ width 48px
+ height 48px
+ border-radius 6px
+ //position -webkit-sticky
+ //position sticky
+ //top 62px
@media (min-width 500px)
margin-right 16px
-
- > .avatar
- display block
- width 48px
- height 48px
- margin 0
- border-radius 6px
- vertical-align bottom
-
- @media (min-width 500px)
- width 58px
- height 58px
- border-radius 8px
+ width 58px
+ height 58px
+ border-radius 8px
> .main
float left
@@ -336,7 +333,7 @@ export default Vue.extend({
> header
display flex
- align-items center
+ align-items baseline
white-space nowrap
@media (min-width 500px)
@@ -347,7 +344,7 @@ export default Vue.extend({
margin 0 0.5em 0 0
padding 0
overflow hidden
- color #627079
+ color isDark ? #fff : #627079
font-size 1em
font-weight bold
text-decoration none
@@ -360,122 +357,165 @@ export default Vue.extend({
margin 0 0.5em 0 0
padding 1px 6px
font-size 12px
- color #aaa
- border solid 1px #ddd
+ color isDark ? #758188 : #aaa
+ border solid 1px isDark ? #57616f : #ddd
border-radius 3px
> .username
margin 0 0.5em 0 0
- color #ccc
+ overflow hidden
+ text-overflow ellipsis
+ color isDark ? #606984 : #ccc
> .info
margin-left auto
font-size 0.9em
+ > *
+ color isDark ? #606984 : #c0c0c0
+
> .mobile
margin-right 6px
- color #c0c0c0
- > .created-at
- color #c0c0c0
+ > .visibility
+ margin-left 6px
> .body
- > .text
+ > .cw
+ cursor default
display block
margin 0
padding 0
overflow-wrap break-word
font-size 1.1em
- color #717171
-
- >>> .quote
- margin 8px
- padding 6px 12px
- color #aaa
- border-left solid 3px #eee
+ color isDark ? #fff : #717171
- > .reply
+ > .text
margin-right 8px
- color #717171
- > .rp
- margin-left 4px
- font-style oblique
- color #a0bf46
+ > .toggle
+ display inline-block
+ padding 4px 8px
+ font-size 0.7em
+ color isDark ? #393f4f : #fff
+ background isDark ? #687390 : #b1b9c1
+ border-radius 2px
+ cursor pointer
+ user-select none
- [data-is-me]:after
- content "you"
- padding 0 4px
- margin-left 4px
- font-size 80%
- color $theme-color-foreground
- background $theme-color
- border-radius 4px
+ &:hover
+ background isDark ? #707b97 : #bbc4ce
- .mk-url-preview
- margin-top 8px
+ > .content
- > .channel
- margin 0
+ > .text
+ display block
+ margin 0
+ padding 0
+ overflow-wrap break-word
+ font-size 1.1em
+ color isDark ? #fff : #717171
- > .tags
- margin 4px 0 0 0
+ >>> .title
+ display block
+ margin-bottom 4px
+ padding 4px
+ font-size 90%
+ text-align center
+ background isDark ? #2f3944 : #eef1f3
+ border-radius 4px
- > *
- display inline-block
- margin 0 8px 0 0
- padding 2px 8px 2px 16px
- font-size 90%
- color #8d969e
- background #edf0f3
- border-radius 4px
+ >>> .code
+ margin 8px 0
+
+ >>> .quote
+ margin 8px
+ padding 6px 12px
+ color isDark ? #6f808e : #aaa
+ border-left solid 3px isDark ? #637182 : #eee
+
+ > .reply
+ margin-right 8px
+ color isDark ? #99abbf : #717171
+
+ > .rp
+ margin-left 4px
+ font-style oblique
+ color #a0bf46
+
+ [data-is-me]:after
+ content "you"
+ padding 0 4px
+ margin-left 4px
+ font-size 80%
+ color $theme-color-foreground
+ background $theme-color
+ border-radius 4px
+
+ .mk-url-preview
+ margin-top 8px
+
+ > .channel
+ margin 0
+
+ > .tags
+ margin 4px 0 0 0
+
+ > *
+ display inline-block
+ margin 0 8px 0 0
+ padding 2px 8px 2px 16px
+ font-size 90%
+ color #8d969e
+ background #edf0f3
+ border-radius 4px
+
+ &:before
+ content ""
+ display block
+ position absolute
+ top 0
+ bottom 0
+ left 4px
+ width 8px
+ height 8px
+ margin auto 0
+ background #fff
+ border-radius 100%
- &:before
- content ""
+ > .media
+ > img
display block
- position absolute
- top 0
- bottom 0
- left 4px
- width 8px
- height 8px
- margin auto 0
- background #fff
- border-radius 100%
+ max-width 100%
- > .media
- > img
- display block
- max-width 100%
+ > .location
+ margin 4px 0
+ font-size 12px
+ color #ccc
- > .location
- margin 4px 0
- font-size 12px
- color #ccc
+ > .map
+ width 100%
+ height 200px
+
+ &:empty
+ display none
+
+ > .mk-poll
+ font-size 80%
- > .map
- width 100%
- height 200px
+ > .renote
+ margin 8px 0
- &:empty
- display none
+ > .mk-note-preview
+ padding 16px
+ border dashed 1px isDark ? #4e945e : #c0dac6
+ border-radius 8px
> .app
font-size 12px
color #ccc
- > .mk-poll
- font-size 80%
-
- > .renote
- margin 8px 0
-
- > .mk-note-preview
- padding 16px
- border dashed 1px #c0dac6
- border-radius 8px
-
> footer
> button
margin 0
@@ -484,14 +524,14 @@ export default Vue.extend({
border none
box-shadow none
font-size 1em
- color #ddd
+ color isDark ? #606984 : #ddd
cursor pointer
&:not(:last-child)
margin-right 28px
&:hover
- color #666
+ color isDark ? #9198af : #666
> .count
display inline
@@ -505,6 +545,12 @@ export default Vue.extend({
@media (max-width 350px)
display none
+.note[data-darkmode]
+ root(true)
+
+.note:not([data-darkmode])
+ root(false)
+
</style>
<style lang="stylus" module>
diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue
index 573026d53e..53e232e521 100644
--- a/src/client/app/mobile/views/components/notes.vue
+++ b/src/client/app/mobile/views/components/notes.vue
@@ -1,30 +1,64 @@
<template>
<div class="mk-notes">
+ <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
+
<slot name="head"></slot>
- <slot></slot>
- <template v-for="(note, i) in _notes">
- <mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
- <p class="date" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
- <span>%fa:angle-up%{{ note._datetext }}</span>
- <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
- </p>
- </template>
- <footer>
- <slot name="tail"></slot>
+
+ <slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot>
+
+ <div class="init" v-if="fetching">
+ %fa:spinner .pulse%%i18n:common.loading%
+ </div>
+
+ <div v-if="!fetching && requestInitPromise != null">
+ <p>読み込みに失敗しました。</p>
+ <button @click="resolveInitPromise">リトライ</button>
+ </div>
+
+ <transition-group name="mk-notes" class="transition">
+ <template v-for="(note, i) in _notes">
+ <mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
+ <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
+ <span>%fa:angle-up%{{ note._datetext }}</span>
+ <span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
+ </p>
+ </template>
+ </transition-group>
+
+ <footer v-if="more">
+ <button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+ <template v-if="!moreFetching">%i18n:@load-more%</template>
+ <template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
+ </button>
</footer>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
+import getNoteSummary from '../../../../../renderers/get-note-summary';
+
+const displayLimit = 30;
export default Vue.extend({
props: {
- notes: {
- type: Array,
- default: () => []
+ more: {
+ type: Function,
+ required: false
}
},
+
+ data() {
+ return {
+ requestInitPromise: null as () => Promise<any[]>,
+ notes: [],
+ queue: [],
+ unreadCount: 0,
+ fetching: true,
+ moreFetching: false
+ };
+ },
+
computed: {
_notes(): any[] {
return (this.notes as any).map(note => {
@@ -36,9 +70,132 @@ export default Vue.extend({
});
}
},
+
+ mounted() {
+ document.addEventListener('visibilitychange', this.onVisibilitychange, false);
+ window.addEventListener('scroll', this.onScroll);
+ },
+
+ beforeDestroy() {
+ document.removeEventListener('visibilitychange', this.onVisibilitychange);
+ window.removeEventListener('scroll', this.onScroll);
+ },
+
methods: {
+ isScrollTop() {
+ return window.scrollY <= 8;
+ },
+
onNoteUpdated(i, note) {
Vue.set((this as any).notes, i, note);
+ },
+
+ init(promiseGenerator: () => Promise<any[]>) {
+ this.requestInitPromise = promiseGenerator;
+ this.resolveInitPromise();
+ },
+
+ resolveInitPromise() {
+ this.queue = [];
+ this.notes = [];
+ this.fetching = true;
+
+ const promise = this.requestInitPromise();
+
+ promise.then(notes => {
+ this.notes = notes;
+ this.requestInitPromise = null;
+ this.fetching = false;
+ }, e => {
+ this.fetching = false;
+ });
+ },
+
+ prepend(note, silent = false) {
+ //#region 弾く
+ const isMyNote = note.userId == (this as any).os.i.id;
+ const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
+
+ if ((this as any).clientSettings.showMyRenotes === false) {
+ if (isMyNote && isPureRenote) {
+ return;
+ }
+ }
+
+ if ((this as any).clientSettings.showRenotedMyNotes === false) {
+ if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) {
+ return;
+ }
+ }
+ //#endregion
+
+ // 投稿が自分のものではないかつ、タブが非表示またはスクロール位置が最上部ではないならタイトルで通知
+ if ((document.hidden || !this.isScrollTop()) && note.userId !== (this as any).os.i.id) {
+ this.unreadCount++;
+ document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
+ }
+
+ if (this.isScrollTop()) {
+ // Prepend the note
+ this.notes.unshift(note);
+
+ // オーバーフローしたら古い投稿は捨てる
+ if (this.notes.length >= displayLimit) {
+ this.notes = this.notes.slice(0, displayLimit);
+ }
+ } else {
+ this.queue.push(note);
+ }
+ },
+
+ append(note) {
+ this.notes.push(note);
+ },
+
+ tail() {
+ return this.notes[this.notes.length - 1];
+ },
+
+ releaseQueue() {
+ this.queue.forEach(n => this.prepend(n, true));
+ this.queue = [];
+ },
+
+ async loadMore() {
+ if (this.more == null) return;
+ if (this.moreFetching) return;
+
+ this.moreFetching = true;
+ await this.more();
+ this.moreFetching = false;
+ },
+
+ clearNotification() {
+ this.unreadCount = 0;
+ document.title = 'Misskey';
+ },
+
+ onVisibilitychange() {
+ if (!document.hidden) {
+ this.clearNotification();
+ }
+ },
+
+ onScroll() {
+ if (this.isScrollTop()) {
+ this.releaseQueue();
+ this.clearNotification();
+ }
+
+ if ((this as any).clientSettings.fetchOnScroll !== false) {
+ // 親要素が display none だったら弾く
+ // https://github.com/syuilo/misskey/issues/1569
+ // http://d.hatena.ne.jp/favril/20091105/1257403319
+ if (this.$el.offsetHeight == 0) return;
+
+ const current = window.scrollY + window.innerHeight;
+ if (current > document.body.offsetHeight - 8) this.loadMore();
+ }
}
}
});
@@ -47,10 +204,46 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-notes
- background #fff
+root(isDark)
+ overflow hidden
+ background isDark ? #282C37 : #fff
border-radius 8px
- box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+ box-shadow 0 0 2px rgba(#000, 0.1)
+
+ @media (min-width 500px)
+ box-shadow 0 8px 32px rgba(#000, 0.1)
+
+ .transition
+ .mk-notes-enter
+ .mk-notes-leave-to
+ opacity 0
+ transform translateY(-30px)
+
+ > *
+ transition transform .3s ease, opacity .3s ease
+
+ > .date
+ display block
+ margin 0
+ line-height 32px
+ text-align center
+ font-size 0.9em
+ color isDark ? #666b79 : #aaa
+ background isDark ? #242731 : #fdfdfd
+ border-bottom solid 1px isDark ? #1c2023 : #eaeaea
+
+ span
+ margin 0 16px
+
+ [data-fa]
+ margin-right 8px
+
+ > .newer-indicator
+ position -webkit-sticky
+ position sticky
+ z-index 100
+ height 3px
+ background $theme-color
> .init
padding 64px 0
@@ -73,27 +266,9 @@ export default Vue.extend({
font-size 3em
color #ccc
- > .date
- display block
- margin 0
- line-height 32px
- text-align center
- font-size 0.9em
- color #aaa
- background #fdfdfd
- border-bottom solid 1px #eaeaea
-
- span
- margin 0 16px
-
- [data-fa]
- margin-right 8px
-
> footer
text-align center
- border-top solid 1px #eaeaea
- border-bottom-left-radius 4px
- border-bottom-right-radius 4px
+ border-top solid 1px isDark ? #1c2023 : #eaeaea
&:empty
display none
@@ -102,10 +277,18 @@ export default Vue.extend({
margin 0
padding 16px
width 100%
- color $theme-color
- border-radius 0 0 8px 8px
+ color #ccc
+
+ @media (min-width 500px)
+ padding 20px
&:disabled
opacity 0.7
+.mk-notes[data-darkmode]
+ root(true)
+
+.mk-notes:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index 4f7c8968b2..c1b37563ce 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -1,15 +1,13 @@
<template>
<div class="mk-notification">
<div class="notification reaction" v-if="notification.type == 'reaction'">
- <mk-time :time="notification.createdAt"/>
- <router-link class="avatar-anchor" :to="notification.user | userPage">
- <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
- </router-link>
- <div class="text">
- <p>
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div>
+ <header>
<mk-reaction-icon :reaction="notification.reaction"/>
<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
- </p>
+ <mk-time :time="notification.createdAt"/>
+ </header>
<router-link class="note-ref" :to="notification.note | notePage">
%fa:quote-left%{{ getNoteSummary(notification.note) }}
%fa:quote-right%
@@ -18,61 +16,55 @@
</div>
<div class="notification renote" v-if="notification.type == 'renote'">
- <mk-time :time="notification.createdAt"/>
- <router-link class="avatar-anchor" :to="notification.user | userPage">
- <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
- </router-link>
- <div class="text">
- <p>
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div>
+ <header>
%fa:retweet%
<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
- </p>
+ <mk-time :time="notification.createdAt"/>
+ </header>
<router-link class="note-ref" :to="notification.note | notePage">
%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
</router-link>
</div>
</div>
- <template v-if="notification.type == 'quote'">
- <mk-note :note="notification.note"/>
- </template>
-
<div class="notification follow" v-if="notification.type == 'follow'">
- <mk-time :time="notification.createdAt"/>
- <router-link class="avatar-anchor" :to="notification.user | userPage">
- <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
- </router-link>
- <div class="text">
- <p>
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div>
+ <header>
%fa:user-plus%
<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
- </p>
+ <mk-time :time="notification.createdAt"/>
+ </header>
</div>
</div>
- <template v-if="notification.type == 'reply'">
- <mk-note :note="notification.note"/>
- </template>
-
- <template v-if="notification.type == 'mention'">
- <mk-note :note="notification.note"/>
- </template>
-
<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
- <mk-time :time="notification.createdAt"/>
- <router-link class="avatar-anchor" :to="notification.user | userPage">
- <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
- </router-link>
- <div class="text">
- <p>
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div>
+ <header>
%fa:chart-pie%
<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
- </p>
+ <mk-time :time="notification.createdAt"/>
+ </header>
<router-link class="note-ref" :to="notification.note | notePage">
%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
</router-link>
</div>
</div>
+
+ <template v-if="notification.type == 'quote'">
+ <mk-note :note="notification.note"/>
+ </template>
+
+ <template v-if="notification.type == 'reply'">
+ <mk-note :note="notification.note"/>
+ </template>
+
+ <template v-if="notification.type == 'mention'">
+ <mk-note :note="notification.note"/>
+ </template>
</div>
</template>
@@ -91,53 +83,63 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-notification
-
+root(isDark)
> .notification
padding 16px
+ font-size 12px
overflow-wrap break-word
+ @media (min-width 350px)
+ font-size 14px
+
+ @media (min-width 500px)
+ font-size 16px
+
+ @media (min-width 600px)
+ padding 24px 32px
+
&:after
content ""
display block
clear both
- > .mk-time
- display inline
- position absolute
- top 16px
- right 12px
- vertical-align top
- color rgba(0, 0, 0, 0.6)
- font-size 0.9em
-
- > .avatar-anchor
+ > .avatar
display block
float left
+ width 36px
+ height 36px
+ border-radius 6px
- img
- min-width 36px
- min-height 36px
- max-width 36px
- max-height 36px
- border-radius 6px
+ @media (min-width 500px)
+ width 42px
+ height 42px
- > .text
+ > div
float right
width calc(100% - 36px)
padding-left 8px
- p
- margin 0
+ @media (min-width 500px)
+ width calc(100% - 42px)
+
+ > header
+ display flex
+ align-items baseline
+ white-space nowrap
i, .mk-reaction-icon
margin-right 4px
+ > .mk-time
+ margin-left auto
+ color isDark ? #606984 : #c0c0c0
+ font-size 0.9em
+
> .note-preview
- color rgba(0, 0, 0, 0.7)
+ color isDark ? #fff : #717171
> .note-ref
- color rgba(0, 0, 0, 0.7)
+ color isDark ? #fff : #717171
[data-fa]
font-size 1em
@@ -147,12 +149,17 @@ export default Vue.extend({
margin-right 3px
&.renote
- .text p i
+ > div > header i
color #77B255
&.follow
- .text p i
+ > div > header i
color #53c7ce
-</style>
+.mk-notification[data-darkmode]
+ root(true)
+.mk-notification:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue
index ad43a27b98..8ab66940c4 100644
--- a/src/client/app/mobile/views/components/notifications.vue
+++ b/src/client/app/mobile/views/components/notifications.vue
@@ -1,18 +1,20 @@
<template>
<div class="mk-notifications">
- <div class="notifications" v-if="notifications.length != 0">
+ <transition-group name="mk-notifications" class="transition notifications">
<template v-for="(notification, i) in _notifications">
<mk-notification :notification="notification" :key="notification.id"/>
- <p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date">
+ <p class="date" :key="notification.id + '_date'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date">
<span>%fa:angle-up%{{ notification._datetext }}</span>
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
</p>
</template>
- </div>
+ </transition-group>
+
<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%' }}
</button>
+
<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
</div>
@@ -101,28 +103,29 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-notifications
- margin 8px auto
- padding 0
- max-width 500px
- width calc(100% - 16px)
- background #fff
+root(isDark)
+ margin 0 auto
+ background isDark ? #282C37 :#fff
border-radius 8px
- box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+ box-shadow 0 0 2px rgba(#000, 0.1)
+ overflow hidden
@media (min-width 500px)
- margin 16px auto
- width calc(100% - 32px)
+ box-shadow 0 8px 32px rgba(#000, 0.1)
- > .notifications
+ .transition
+ .mk-notifications-enter
+ .mk-notifications-leave-to
+ opacity 0
+ transform translateY(-30px)
- > .mk-notification
- margin 0 auto
- max-width 500px
- border-bottom solid 1px rgba(0, 0, 0, 0.05)
+ > *
+ transition transform .3s ease, opacity .3s ease
- &:last-child
- border-bottom none
+ > .notifications
+
+ > .mk-notification:not(:last-child)
+ border-bottom solid 1px isDark ? #1c2023 : #eaeaea
> .date
display block
@@ -130,9 +133,9 @@ export default Vue.extend({
line-height 32px
text-align center
font-size 0.8em
- color #aaa
- background #fdfdfd
- border-bottom solid 1px rgba(0, 0, 0, 0.05)
+ color isDark ? #666b79 : #aaa
+ background isDark ? #242731 : #fdfdfd
+ border-bottom solid 1px isDark ? #1c2023 : #eaeaea
span
margin 0 16px
@@ -145,7 +148,7 @@ export default Vue.extend({
width 100%
padding 16px
color #555
- border-top solid 1px rgba(0, 0, 0, 0.05)
+ border-top solid 1px rgba(#000, 0.05)
> [data-fa]
margin-right 4px
@@ -165,4 +168,10 @@ export default Vue.extend({
> [data-fa]
margin-right 4px
+.mk-notifications[data-darkmode]
+ root(true)
+
+.mk-notifications:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index 861e8653ba..6d80b3046b 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -10,6 +10,11 @@
</header>
<div class="form">
<mk-note-preview v-if="reply" :note="reply"/>
+ <div v-if="visibility == 'specified'" class="visibleUsers">
+ <span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
+ <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%' : '%i18n:!@note-placeholder%'"></textarea>
<div class="attaches" v-show="files.length != 0">
<x-draggable class="files" :list="files" :options="{ animation: 150 }">
@@ -20,11 +25,15 @@
</div>
<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false"/>
<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
- <button class="upload" @click="chooseFile">%fa:upload%</button>
- <button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button>
- <button class="kao" @click="kao">%fa:R smile%</button>
- <button class="poll" @click="poll = true">%fa:chart-pie%</button>
- <button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
+ <footer>
+ <button class="upload" @click="chooseFile">%fa:upload%</button>
+ <button class="drive" @click="chooseFileFromDrive">%fa:cloud%</button>
+ <button class="kao" @click="kao">%fa:R smile%</button>
+ <button class="poll" @click="poll = true">%fa:chart-pie%</button>
+ <button class="poll" @click="useCw = !useCw">%fa:eye-slash%</button>
+ <button class="geo" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
+ <button class="visibility" @click="setVisibility" ref="visibilityButton">%fa:lock%</button>
+ </footer>
<input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
</div>
</div>
@@ -33,13 +42,17 @@
<script lang="ts">
import Vue from 'vue';
import * as XDraggable from 'vuedraggable';
+import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import getKao from '../../../common/scripts/get-kao';
export default Vue.extend({
components: {
- XDraggable
+ XDraggable,
+ MkVisibilityChooser
},
+
props: ['reply'],
+
data() {
return {
posting: false,
@@ -47,21 +60,33 @@ export default Vue.extend({
uploadings: [],
files: [],
poll: false,
- geo: null
+ geo: null,
+ visibility: 'public',
+ visibleUsers: [],
+ useCw: false,
+ cw: null
};
},
+
mounted() {
+ if (this.reply && this.reply.user.host != null) {
+ this.text = `@${this.reply.user.username}@${this.reply.user.host} `;
+ }
+
this.$nextTick(() => {
this.focus();
});
},
+
methods: {
focus() {
(this.$refs.text as any).focus();
},
+
chooseFile() {
(this.$refs.file as any).click();
},
+
chooseFileFromDrive() {
(this as any).apis.chooseDriveFile({
multiple: true
@@ -69,23 +94,29 @@ export default Vue.extend({
files.forEach(this.attachMedia);
});
},
+
attachMedia(driveFile) {
this.files.push(driveFile);
this.$emit('change-attached-media', this.files);
},
+
detachMedia(file) {
this.files = this.files.filter(x => x.id != file.id);
this.$emit('change-attached-media', this.files);
},
+
onChangeFile() {
Array.from((this.$refs.file as any).files).forEach(this.upload);
},
+
upload(file) {
(this.$refs.uploader as any).upload(file);
},
+
onChangeUploadings(uploads) {
this.$emit('change-uploadings', uploads);
},
+
setGeo() {
if (navigator.geolocation == null) {
alert('お使いの端末は位置情報に対応していません');
@@ -100,23 +131,54 @@ export default Vue.extend({
enableHighAccuracy: true
});
},
+
removeGeo() {
this.geo = null;
},
+
+ setVisibility() {
+ const w = (this as any).os.new(MkVisibilityChooser, {
+ source: this.$refs.visibilityButton,
+ compact: true,
+ v: this.visibility
+ });
+ w.$once('chosen', v => {
+ this.visibility = v;
+ });
+ },
+
+ addVisibleUser() {
+ (this as any).apis.input({
+ title: 'ユーザー名を入力してください'
+ }).then(username => {
+ (this as any).api('users/show', {
+ username
+ }).then(user => {
+ this.visibleUsers.push(user);
+ });
+ });
+ },
+
+ removeVisibleUser(user) {
+ this.visibleUsers = this.visibleUsers.filter(u => u != user);
+ },
+
clear() {
this.text = '';
this.files = [];
this.poll = false;
this.$emit('change-attached-media');
},
+
post() {
this.posting = true;
- const viaMobile = (this as any).os.i.clientSettings.disableViaMobile !== true;
+ const viaMobile = (this as any).clientSettings.disableViaMobile !== true;
(this as any).api('notes/create', {
text: this.text == '' ? undefined : this.text,
mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
+ cw: this.useCw ? this.cw || '' : undefined,
geo: this.geo ? {
coordinates: [this.geo.longitude, this.geo.latitude],
altitude: this.geo.altitude,
@@ -125,6 +187,8 @@ export default Vue.extend({
heading: isNaN(this.geo.heading) ? null : this.geo.heading,
speed: this.geo.speed,
} : null,
+ visibility: this.visibility,
+ visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
viaMobile: viaMobile
}).then(data => {
this.$emit('note');
@@ -133,10 +197,12 @@ export default Vue.extend({
this.posting = false;
});
},
+
cancel() {
this.$emit('cancel');
this.$destroy();
},
+
kao() {
this.text += getKao();
}
@@ -147,29 +213,33 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.mk-post-form
+root(isDark)
max-width 500px
width calc(100% - 16px)
margin 8px auto
- background #fff
+ background isDark ? #282C37 : #fff
border-radius 8px
- box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+ box-shadow 0 0 2px rgba(#000, 0.1)
@media (min-width 500px)
margin 16px auto
width calc(100% - 32px)
+ box-shadow 0 8px 32px rgba(#000, 0.1)
+
+ @media (min-width 600px)
+ margin 32px auto
> header
- z-index 1
+ z-index 1000
height 50px
- box-shadow 0 1px 0 0 rgba(0, 0, 0, 0.1)
+ box-shadow 0 1px 0 0 isDark ? rgba(#000, 0.2) : rgba(#000, 0.1)
> .cancel
padding 0
width 50px
line-height 50px
font-size 24px
- color #555
+ color isDark ? #9baec8 : #555
> div
position absolute
@@ -203,6 +273,38 @@ export default Vue.extend({
> .mk-note-preview
padding 16px
+ > .visibleUsers
+ margin-bottom 8px
+ font-size 14px
+
+ > span
+ margin-right 16px
+ color isDark ? #fff : #666
+
+ > input
+ z-index 1
+
+ > input
+ > textarea
+ display block
+ padding 12px
+ margin 0
+ width 100%
+ font-size 16px
+ color isDark ? #fff : #333
+ background isDark ? #191d23 : #fff
+ border none
+ border-radius 0
+ box-shadow 0 1px 0 0 isDark ? rgba(#000, 0.2) : rgba(#000, 0.1)
+
+ &:disabled
+ opacity 0.5
+
+ > textarea
+ max-width 100%
+ min-width 100%
+ min-height 80px
+
> .attaches
> .files
@@ -236,40 +338,30 @@ export default Vue.extend({
> .file
display none
- > textarea
- display block
- padding 12px
- margin 0
- width 100%
- max-width 100%
- min-width 100%
- min-height 80px
- font-size 16px
- color #333
- border none
- border-bottom solid 1px #ddd
- border-radius 0
+ > footer
+ white-space nowrap
+ overflow auto
+ -webkit-overflow-scrolling touch
+ overflow-scrolling touch
- &:disabled
- opacity 0.5
+ > *
+ display inline-block
+ padding 0
+ margin 0
+ width 48px
+ height 48px
+ font-size 20px
+ color #657786
+ background transparent
+ outline none
+ border none
+ border-radius 0
+ box-shadow none
- > .upload
- > .drive
- > .kao
- > .poll
- > .geo
- display inline-block
- padding 0
- margin 0
- width 48px
- height 48px
- font-size 20px
- color #657786
- background transparent
- outline none
- border none
- border-radius 0
- box-shadow none
+.mk-post-form[data-darkmode]
+ root(true)
-</style>
+.mk-post-form:not([data-darkmode])
+ root(false)
+</style>
diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue
index 54cc74f7f5..cc50977a58 100644
--- a/src/client/app/mobile/views/components/sub-note-content.vue
+++ b/src/client/app/mobile/views/components/sub-note-content.vue
@@ -1,6 +1,7 @@
<template>
<div class="mk-sub-note-content">
<div class="body">
+ <span v-if="note.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
<a class="reply" v-if="note.replyId">%fa:reply%</a>
<mk-note-html v-if="note.text" :text="note.text" :i="os.i"/>
<a class="rp" v-if="note.renoteId">RP: ...</a>
diff --git a/src/client/app/mobile/views/components/timeline.vue b/src/client/app/mobile/views/components/timeline.vue
deleted file mode 100644
index 11b82aa456..0000000000
--- a/src/client/app/mobile/views/components/timeline.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-<template>
-<div class="mk-timeline">
- <mk-friends-maker v-if="alone"/>
- <mk-notes :notes="notes">
- <div class="init" v-if="fetching">
- %fa:spinner .pulse%%i18n:common.loading%
- </div>
- <div class="empty" v-if="!fetching && notes.length == 0">
- %fa:R comments%
- %i18n:@empty%
- </div>
- <button v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail">
- <span v-if="!moreFetching">%i18n:@load-more%</span>
- <span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
- </button>
- </mk-notes>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-const limit = 10;
-
-export default Vue.extend({
- props: {
- date: {
- type: Date,
- required: false,
- default: null
- }
- },
- data() {
- return {
- fetching: true,
- moreFetching: false,
- notes: [],
- existMore: false,
- connection: null,
- connectionId: null
- };
- },
- computed: {
- alone(): boolean {
- return (this as any).os.i.followingCount == 0;
- }
- },
- mounted() {
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
-
- this.connection.on('note', this.onNote);
- this.connection.on('follow', this.onChangeFollowing);
- this.connection.on('unfollow', this.onChangeFollowing);
-
-this.fetch();
- },
- beforeDestroy() {
- this.connection.off('note', this.onNote);
- this.connection.off('follow', this.onChangeFollowing);
- this.connection.off('unfollow', this.onChangeFollowing);
- (this as any).os.stream.dispose(this.connectionId);
- },
- methods: {
- fetch(cb?) {
- this.fetching = true;
- (this as any).api('notes/timeline', {
- limit: limit + 1,
- untilDate: this.date ? (this.date as any).getTime() : undefined
- }).then(notes => {
- if (notes.length == limit + 1) {
- notes.pop();
- this.existMore = true;
- }
- this.notes = notes;
- this.fetching = false;
- this.$emit('loaded');
- if (cb) cb();
- });
- },
- more() {
- this.moreFetching = true;
- (this as any).api('notes/timeline', {
- limit: limit + 1,
- untilId: this.notes[this.notes.length - 1].id
- }).then(notes => {
- if (notes.length == limit + 1) {
- notes.pop();
- this.existMore = true;
- } else {
- this.existMore = false;
- }
- this.notes = this.notes.concat(notes);
- this.moreFetching = false;
- });
- },
- onNote(note) {
- this.notes.unshift(note);
-
- const isTop = window.scrollY > 8;
- if (isTop) this.notes.pop();
- },
- onChangeFollowing() {
- this.fetch();
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-friends-maker
- margin-bottom 8px
-</style>
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
index f1b24bf2da..509463333d 100644
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ b/src/client/app/mobile/views/components/ui.header.vue
@@ -32,6 +32,8 @@ export default Vue.extend({
};
},
mounted() {
+ this.$store.commit('setUiHeaderHeight', 48);
+
if ((this as any).os.isSignedIn) {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
@@ -57,9 +59,10 @@ export default Vue.extend({
}
});
- const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000
+ const ago = (new Date().getTime() - new Date((this as any).os.i.lastUsedAt).getTime()) / 1000;
const isHisasiburi = ago >= 3600;
(this as any).os.i.lastUsedAt = new Date();
+ (this as any).os.bakeMe();
if (isHisasiburi) {
(this.$refs.welcomeback as any).style.display = 'block';
(this.$refs.main as any).style.overflow = 'hidden';
@@ -141,7 +144,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.header
+root(isDark)
$height = 48px
position fixed
@@ -150,6 +153,9 @@ export default Vue.extend({
width 100%
box-shadow 0 1px 0 rgba(#000, 0.075)
+ &, *
+ user-select none
+
> .main
color rgba(#fff, 0.9)
@@ -162,7 +168,7 @@ export default Vue.extend({
-webkit-backdrop-filter blur(12px)
backdrop-filter blur(12px)
//background-color rgba(#1b2023, 0.75)
- background-color #1b2023
+ background-color isDark ? #313543 : #595f6f
> p
display none
@@ -239,4 +245,10 @@ export default Vue.extend({
line-height $height
border-left solid 1px rgba(#000, 0.1)
+.header[data-darkmode]
+ root(true)
+
+.header:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index 68cdacb3b5..5c65d52237 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -15,19 +15,20 @@
</router-link>
<div class="links">
<ul>
- <li><router-link to="/">%fa:home%%i18n:@home%%fa:angle-right%</router-link></li>
- <li><router-link to="/i/notifications">%fa:R bell%%i18n:@notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</router-link></li>
- <li><router-link to="/i/messaging">%fa:R comments%%i18n:@messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</router-link></li>
- <li><router-link to="/othello">%fa:gamepad%ゲーム<template v-if="hasGameInvitations">%fa:circle%</template>%fa:angle-right%</router-link></li>
+ <li><router-link to="/" :data-active="$route.name == 'index'">%fa:home%%i18n:@home%%fa:angle-right%</router-link></li>
+ <li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'">%fa:R bell%%i18n:@notifications%<template v-if="hasUnreadNotifications">%fa:circle%</template>%fa:angle-right%</router-link></li>
+ <li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'">%fa:R comments%%i18n:@messaging%<template v-if="hasUnreadMessagingMessages">%fa:circle%</template>%fa:angle-right%</router-link></li>
+ <li><router-link to="/othello" :data-active="$route.name == 'othello'">%fa:gamepad%ゲーム<template v-if="hasGameInvitations">%fa:circle%</template>%fa:angle-right%</router-link></li>
</ul>
<ul>
- <li><router-link to="/i/drive">%fa:cloud%%i18n:@drive%%fa:angle-right%</router-link></li>
+ <li><router-link to="/i/drive" :data-active="$route.name == 'drive'">%fa:cloud%%i18n:@drive%%fa:angle-right%</router-link></li>
</ul>
<ul>
<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>
</ul>
</div>
<a :href="aboutUrl"><p class="about">%i18n:@about%</p></a>
@@ -113,6 +114,9 @@ export default Vue.extend({
},
onOthelloNoInvites() {
this.hasGameInvitations = false;
+ },
+ dark() {
+ (this as any)._updateDarkmode_(!(this as any)._darkmode_);
}
}
});
@@ -121,7 +125,9 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.nav
+root(isDark)
+ $color = isDark ? #c9d2e0 : #777
+
.backdrop
position fixed
top 0
@@ -129,7 +135,7 @@ export default Vue.extend({
z-index 1025
width 100%
height 100%
- background rgba(0, 0, 0, 0.2)
+ background isDark ? rgba(#000, 0.7) : rgba(#000, 0.2)
.body
position fixed
@@ -140,8 +146,7 @@ export default Vue.extend({
height 100%
overflow auto
-webkit-overflow-scrolling touch
- color #777
- background #fff
+ background isDark ? #16191f : #fff
.me
display block
@@ -162,7 +167,7 @@ export default Vue.extend({
left 80px
padding 0
width calc(100% - 112px)
- color #777
+ color $color
line-height 96px
overflow hidden
text-overflow ellipsis
@@ -182,14 +187,22 @@ export default Vue.extend({
font-size 1em
line-height 1em
- a
+ a, p
display block
+ margin 0
padding 0 20px
line-height 3rem
line-height calc(1rem + 30px)
- color #777
+ color $color
text-decoration none
+ &[data-active]
+ color $theme-color-foreground
+ background $theme-color
+
+ > [data-fa]:last-child
+ color $theme-color-foreground
+
> [data-fa]:first-child
margin-right 0.5em
@@ -205,18 +218,17 @@ export default Vue.extend({
padding 0 20px
font-size 1.2em
line-height calc(1rem + 30px)
- color #ccc
+ color $color
+ opacity 0.5
.about
margin 0
padding 1em 0
text-align center
font-size 0.8em
+ color $color
opacity 0.5
- a
- color #777
-
.nav-enter-active,
.nav-leave-active {
opacity: 1;
@@ -239,4 +251,10 @@ export default Vue.extend({
opacity: 0;
}
+.nav[data-darkmode]
+ root(true)
+
+.nav:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/mobile/views/components/user-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue
new file mode 100644
index 0000000000..59d6abbbc1
--- /dev/null
+++ b/src/client/app/mobile/views/components/user-list-timeline.vue
@@ -0,0 +1,93 @@
+<template>
+<div>
+ <mk-notes ref="timeline" :more="existMore ? more : null"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { UserListStream } from '../../../common/scripts/streaming/user-list';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ props: ['list'],
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null
+ };
+ },
+ watch: {
+ $route: 'init'
+ },
+ mounted() {
+ this.init();
+ },
+ beforeDestroy() {
+ this.connection.close();
+ },
+ methods: {
+ init() {
+ if (this.connection) this.connection.close();
+ this.connection = new UserListStream((this as any).os, (this as any).os.i, this.list.id);
+ this.connection.on('note', this.onNote);
+ this.connection.on('userAdded', this.onUserAdded);
+ this.connection.on('userRemoved', this.onUserRemoved);
+
+ this.fetch();
+ },
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('notes/user-list-timeline', {
+ listId: this.list.id,
+ limit: fetchLimit + 1,
+ includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+ includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ this.$emit('loaded');
+ }, rej);
+ }));
+ },
+ more() {
+ this.moreFetching = true;
+
+ (this as any).api('notes/user-list-timeline', {
+ listId: this.list.id,
+ limit: fetchLimit + 1,
+ untilId: (this.$refs.timeline as any).tail().id,
+ includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+ includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
+ this.moreFetching = false;
+ });
+ },
+ onNote(note) {
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
+ },
+ onUserAdded() {
+ this.fetch();
+ },
+ onUserRemoved() {
+ this.fetch();
+ }
+ }
+});
+</script>
diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue
index 23a83b5e3a..d258360911 100644
--- a/src/client/app/mobile/views/components/user-preview.vue
+++ b/src/client/app/mobile/views/components/user-preview.vue
@@ -1,8 +1,6 @@
<template>
<div class="mk-user-preview">
- <router-link class="avatar-anchor" :to="user | userPage">
- <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
- </router-link>
+ <mk-avatar class="avatar" :user="user"/>
<div class="main">
<header>
<router-link class="name" :to="user | userPage">{{ user | userName }}</router-link>
@@ -40,26 +38,19 @@ export default Vue.extend({
display block
clear both
- > .avatar-anchor
+ > .avatar
display block
float left
margin 0 10px 0 0
+ width 48px
+ height 48px
+ border-radius 6px
@media (min-width 500px)
margin-right 16px
-
- > .avatar
- display block
- width 48px
- height 48px
- margin 0
- border-radius 6px
- vertical-align bottom
-
- @media (min-width 500px)
- width 58px
- height 58px
- border-radius 8px
+ width 58px
+ height 58px
+ border-radius 8px
> .main
float left
diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue
index 40b3be035e..3ceb876596 100644
--- a/src/client/app/mobile/views/components/user-timeline.vue
+++ b/src/client/app/mobile/views/components/user-timeline.vue
@@ -1,17 +1,10 @@
<template>
<div class="mk-user-timeline">
- <mk-notes :notes="notes">
- <div class="init" v-if="fetching">
- %fa:spinner .pulse%%i18n:common.loading%
- </div>
- <div class="empty" v-if="!fetching && notes.length == 0">
+ <mk-notes ref="timeline" :more="existMore ? more : null">
+ <div slot="empty">
%fa:R comments%
{{ withMedia ? '%i18n:!@no-notes-with-media%' : '%i18n:!@no-notes%' }}
</div>
- <button v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail">
- <span v-if="!moreFetching">%i18n:@load-more%</span>
- <span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
- </button>
</mk-notes>
</div>
</template>
@@ -19,58 +12,68 @@
<script lang="ts">
import Vue from 'vue';
-const limit = 10;
+const fetchLimit = 10;
export default Vue.extend({
props: ['user', 'withMedia'],
+
data() {
return {
fetching: true,
- notes: [],
existMore: false,
moreFetching: false
};
},
+
+ computed: {
+ canFetchMore(): boolean {
+ return !this.moreFetching && !this.fetching && this.existMore;
+ }
+ },
+
mounted() {
- (this as any).api('users/notes', {
- userId: this.user.id,
- withMedia: this.withMedia,
- limit: limit + 1
- }).then(notes => {
- if (notes.length == limit + 1) {
- notes.pop();
- this.existMore = true;
- }
- this.notes = notes;
- this.fetching = false;
- this.$emit('loaded');
- });
+ this.fetch();
},
+
methods: {
+ fetch() {
+ this.fetching = true;
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('users/notes', {
+ userId: this.user.id,
+ withMedia: this.withMedia,
+ limit: fetchLimit + 1
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ this.$emit('loaded');
+ }, rej);
+ }));
+ },
+
more() {
+ if (!this.canFetchMore) return;
+
this.moreFetching = true;
(this as any).api('users/notes', {
userId: this.user.id,
withMedia: this.withMedia,
- limit: limit + 1,
- untilId: this.notes[this.notes.length - 1].id
+ limit: fetchLimit + 1,
+ untilId: (this.$refs.timeline as any).tail().id
}).then(notes => {
- if (notes.length == limit + 1) {
+ if (notes.length == fetchLimit + 1) {
notes.pop();
- this.existMore = true;
} else {
this.existMore = false;
}
- this.notes = this.notes.concat(notes);
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
}
}
});
</script>
-
-<style lang="stylus" scoped>
-.mk-user-timeline
- max-width 600px
- margin 0 auto
-</style>
diff --git a/src/client/app/mobile/views/components/users-list.vue b/src/client/app/mobile/views/components/users-list.vue
index 8fa7a9cbe6..6175067459 100644
--- a/src/client/app/mobile/views/components/users-list.vue
+++ b/src/client/app/mobile/views/components/users-list.vue
@@ -1,8 +1,8 @@
<template>
<div class="mk-users-list">
<nav>
- <span :data-is-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span>
- <span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@known%<span>{{ youKnowCount }}</span></span>
+ <span :data-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span>
+ <span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@known%<span>{{ youKnowCount }}</span></span>
</nav>
<div class="users" v-if="!fetching && users.length != 0">
<mk-user-preview v-for="u in users" :user="u" :key="u.id"/>
@@ -74,7 +74,7 @@ export default Vue.extend({
justify-content center
margin 0 auto
max-width 600px
- border-bottom solid 1px rgba(0, 0, 0, 0.2)
+ border-bottom solid 1px rgba(#000, 0.2)
> span
display block
@@ -85,7 +85,7 @@ export default Vue.extend({
color #657786
border-bottom solid 2px transparent
- &[data-is-active]
+ &[data-active]
font-weight bold
color $theme-color
border-color $theme-color
@@ -97,7 +97,7 @@ export default Vue.extend({
font-size 12px
line-height 1
color #fff
- background rgba(0, 0, 0, 0.3)
+ background rgba(#000, 0.3)
border-radius 20px
> .users
@@ -106,14 +106,14 @@ export default Vue.extend({
width calc(100% - 16px)
background #fff
border-radius 8px
- box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+ box-shadow 0 0 0 1px rgba(#000, 0.2)
@media (min-width 500px)
margin 16px auto
width calc(100% - 32px)
> *
- border-bottom solid 1px rgba(0, 0, 0, 0.05)
+ border-bottom solid 1px rgba(#000, 0.05)
> .no
margin 0
diff --git a/src/client/app/mobile/views/components/widget-container.vue b/src/client/app/mobile/views/components/widget-container.vue
index 7319c90849..1bdc875763 100644
--- a/src/client/app/mobile/views/components/widget-container.vue
+++ b/src/client/app/mobile/views/components/widget-container.vue
@@ -28,7 +28,7 @@ export default Vue.extend({
.mk-widget-container
background #eee
border-radius 8px
- box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+ box-shadow 0 0 0 1px rgba(#000, 0.2)
overflow hidden
&.hideHeader
diff --git a/src/client/app/mobile/views/pages/dashboard.vue b/src/client/app/mobile/views/pages/dashboard.vue
new file mode 100644
index 0000000000..a5ca6cb4a2
--- /dev/null
+++ b/src/client/app/mobile/views/pages/dashboard.vue
@@ -0,0 +1,195 @@
+<template>
+<mk-ui>
+ <span slot="header">%fa:home%ダッシュボード</span>
+ <template slot="func">
+ <button @click="customizing = !customizing">%fa:cog%</button>
+ </template>
+ <main>
+ <template v-if="customizing">
+ <header>
+ <select v-model="widgetAdderSelected">
+ <option value="profile">プロフィール</option>
+ <option value="calendar">カレンダー</option>
+ <option value="activity">アクティビティ</option>
+ <option value="rss">RSSリーダー</option>
+ <option value="photo-stream">フォトストリーム</option>
+ <option value="slideshow">スライドショー</option>
+ <option value="version">バージョン</option>
+ <option value="access-log">アクセスログ</option>
+ <option value="server">サーバー情報</option>
+ <option value="donation">寄付のお願い</option>
+ <option value="nav">ナビゲーション</option>
+ <option value="tips">ヒント</option>
+ </select>
+ <button @click="addWidget">追加</button>
+ <p><a @click="hint">カスタマイズのヒント</a></p>
+ </header>
+ <x-draggable
+ :list="widgets"
+ :options="{ handle: '.handle', animation: 150 }"
+ @sort="onWidgetSort"
+ >
+ <div v-for="widget in widgets" class="customize-container" :key="widget.id">
+ <header>
+ <span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
+ </header>
+ <div @click="widgetFunc(widget.id)">
+ <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/>
+ </div>
+ </div>
+ </x-draggable>
+ </template>
+ <template v-else>
+ <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/>
+ </template>
+ </main>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as XDraggable from 'vuedraggable';
+import * as uuid from 'uuid';
+
+export default Vue.extend({
+ components: {
+ XDraggable
+ },
+ data() {
+ return {
+ showNav: false,
+ widgets: [],
+ customizing: false,
+ widgetAdderSelected: null
+ };
+ },
+ created() {
+ if ((this as any).clientSettings.mobileHome == null) {
+ Vue.set((this as any).clientSettings, 'mobileHome', [{
+ name: 'calendar',
+ id: 'a', data: {}
+ }, {
+ name: 'activity',
+ id: 'b', data: {}
+ }, {
+ name: 'rss',
+ id: 'c', data: {}
+ }, {
+ name: 'photo-stream',
+ id: 'd', data: {}
+ }, {
+ name: 'donation',
+ id: 'e', data: {}
+ }, {
+ name: 'nav',
+ id: 'f', data: {}
+ }, {
+ name: 'version',
+ id: 'g', data: {}
+ }]);
+ this.widgets = (this as any).clientSettings.mobileHome;
+ this.saveHome();
+ } else {
+ this.widgets = (this as any).clientSettings.mobileHome;
+ }
+
+ this.$watch('clientSettings', i => {
+ this.widgets = (this as any).clientSettings.mobileHome;
+ }, {
+ deep: true
+ });
+ },
+
+ mounted() {
+ document.title = 'Misskey';
+ },
+
+ methods: {
+ onHomeUpdated(data) {
+ if (data.home) {
+ (this as any).clientSettings.mobileHome = data.home;
+ this.widgets = data.home;
+ } else {
+ const w = (this as any).clientSettings.mobileHome.find(w => w.id == data.id);
+ if (w != null) {
+ w.data = data.data;
+ this.$refs[w.id][0].preventSave = true;
+ this.$refs[w.id][0].props = w.data;
+ this.widgets = (this as any).clientSettings.mobileHome;
+ }
+ }
+ },
+ hint() {
+ alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。');
+ },
+ widgetFunc(id) {
+ const w = this.$refs[id][0];
+ if (w.func) w.func();
+ },
+ onWidgetSort() {
+ this.saveHome();
+ },
+ addWidget() {
+ const widget = {
+ name: this.widgetAdderSelected,
+ id: uuid(),
+ data: {}
+ };
+
+ this.widgets.unshift(widget);
+ this.saveHome();
+ },
+ removeWidget(widget) {
+ this.widgets = this.widgets.filter(w => w.id != widget.id);
+ this.saveHome();
+ },
+ saveHome() {
+ (this as any).clientSettings.mobileHome = this.widgets;
+ (this as any).api('i/update_mobile_home', {
+ home: this.widgets
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+main
+ margin 0 auto
+ max-width 500px
+
+ @media (min-width 500px)
+ padding 8px
+
+ > header
+ padding 8px
+ background #fff
+
+ .widget
+ margin 8px
+
+ .customize-container
+ margin 8px
+ background #fff
+
+ > header
+ line-height 32px
+ background #eee
+
+ > .handle
+ padding 0 8px
+
+ > .remove
+ position absolute
+ top 0
+ right 0
+ padding 0 8px
+ line-height 32px
+
+ > div
+ padding 8px
+
+ > *
+ pointer-events none
+
+</style>
diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue
index f3c75f71e9..33ade94e35 100644
--- a/src/client/app/mobile/views/pages/followers.vue
+++ b/src/client/app/mobile/views/pages/followers.vue
@@ -40,9 +40,6 @@ export default Vue.extend({
created() {
this.fetch();
},
- mounted() {
- document.documentElement.style.background = '#313a42';
- },
methods: {
fetch() {
Progress.start();
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
index 88368ff778..c6d6d44281 100644
--- a/src/client/app/mobile/views/pages/following.vue
+++ b/src/client/app/mobile/views/pages/following.vue
@@ -39,9 +39,6 @@ export default Vue.extend({
created() {
this.fetch();
},
- mounted() {
- document.documentElement.style.background = '#313a42';
- },
methods: {
fetch() {
Progress.start();
diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue
new file mode 100644
index 0000000000..4c1c344db1
--- /dev/null
+++ b/src/client/app/mobile/views/pages/home.timeline.vue
@@ -0,0 +1,149 @@
+<template>
+<div>
+ <mk-friends-maker v-if="src == 'home' && alone" style="margin-bottom:8px"/>
+
+ <mk-notes ref="timeline" :more="existMore ? more : null">
+ <div slot="empty">
+ %fa:R comments%
+ %i18n:@empty%
+ </div>
+ </mk-notes>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ props: {
+ src: {
+ type: String,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null,
+ connectionId: null,
+ unreadCount: 0,
+ date: null
+ };
+ },
+
+ computed: {
+ alone(): boolean {
+ return (this as any).os.i.followingCount == 0;
+ },
+
+ stream(): any {
+ return this.src == 'home'
+ ? (this as any).os.stream
+ : this.src == 'local'
+ ? (this as any).os.streams.localTimelineStream
+ : (this as any).os.streams.globalTimelineStream;
+ },
+
+ endpoint(): string {
+ return this.src == 'home'
+ ? 'notes/timeline'
+ : this.src == 'local'
+ ? 'notes/local-timeline'
+ : 'notes/global-timeline';
+ },
+
+ canFetchMore(): boolean {
+ return !this.moreFetching && !this.fetching && this.existMore;
+ }
+ },
+
+ mounted() {
+ this.connection = this.stream.getConnection();
+ this.connectionId = this.stream.use();
+
+ this.connection.on('note', this.onNote);
+ if (this.src == 'home') {
+ this.connection.on('follow', this.onChangeFollowing);
+ this.connection.on('unfollow', this.onChangeFollowing);
+ }
+
+ this.fetch();
+ },
+
+ beforeDestroy() {
+ this.connection.off('note', this.onNote);
+ if (this.src == 'home') {
+ this.connection.off('follow', this.onChangeFollowing);
+ this.connection.off('unfollow', this.onChangeFollowing);
+ }
+ this.stream.dispose(this.connectionId);
+ },
+
+ methods: {
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api(this.endpoint, {
+ limit: fetchLimit + 1,
+ untilDate: this.date ? this.date.getTime() : undefined,
+ includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+ includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ this.$emit('loaded');
+ }, rej);
+ }));
+ },
+
+ more() {
+ if (!this.canFetchMore) return;
+
+ this.moreFetching = true;
+
+ (this as any).api(this.endpoint, {
+ limit: fetchLimit + 1,
+ untilId: (this.$refs.timeline as any).tail().id,
+ includeMyRenotes: (this as any).clientSettings.showMyRenotes,
+ includeRenotedMyNotes: (this as any).clientSettings.showRenotedMyNotes
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
+ this.moreFetching = false;
+ });
+ },
+
+ onNote(note) {
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
+ },
+
+ onChangeFollowing() {
+ this.fetch();
+ },
+
+ focus() {
+ (this.$refs.timeline as any).focus();
+ },
+
+ warp(date) {
+ this.date = date;
+ this.fetch();
+ }
+ }
+});
+</script>
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index 3d94dd7ce6..ad6d5ed408 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -1,59 +1,42 @@
<template>
<mk-ui>
- <span slot="header" @click="showTl = !showTl">
- <template v-if="showTl">%fa:home%%i18n:@timeline%</template>
- <template v-else>%fa:home%ウィジェット</template>
+ <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.startsWith('list')">%fa:list%{{ list.title }}</span>
+ </span>
<span style="margin-left:8px">
- <template v-if="showTl">%fa:angle-down%</template>
+ <template v-if="!showNav">%fa:angle-down%</template>
<template v-else>%fa:angle-up%</template>
</span>
</span>
+
<template slot="func">
- <button @click="fn" v-if="showTl">%fa:pencil-alt%</button>
- <button @click="customizing = !customizing" v-else>%fa:cog%</button>
+ <button @click="fn">%fa:pencil-alt%</button>
</template>
- <main>
- <div class="tl">
- <mk-timeline @loaded="onLoaded" v-show="showTl"/>
+
+ <main :data-darkmode="_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>
+ <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>
+ </div>
+ </div>
</div>
- <div class="widgets" v-show="!showTl">
- <template v-if="customizing">
- <header>
- <select v-model="widgetAdderSelected">
- <option value="profile">プロフィール</option>
- <option value="calendar">カレンダー</option>
- <option value="activity">アクティビティ</option>
- <option value="rss">RSSリーダー</option>
- <option value="photo-stream">フォトストリーム</option>
- <option value="slideshow">スライドショー</option>
- <option value="version">バージョン</option>
- <option value="access-log">アクセスログ</option>
- <option value="server">サーバー情報</option>
- <option value="donation">寄付のお願い</option>
- <option value="nav">ナビゲーション</option>
- <option value="tips">ヒント</option>
- </select>
- <button @click="addWidget">追加</button>
- <p><a @click="hint">カスタマイズのヒント</a></p>
- </header>
- <x-draggable
- :list="widgets"
- :options="{ handle: '.handle', animation: 150 }"
- @sort="onWidgetSort"
- >
- <div v-for="widget in widgets" class="customize-container" :key="widget.id">
- <header>
- <span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
- </header>
- <div @click="widgetFunc(widget.id)">
- <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/>
- </div>
- </div>
- </x-draggable>
- </template>
- <template v-else>
- <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/>
- </template>
+
+ <div class="tl">
+ <x-tl v-if="src == 'home'" ref="tl" key="home" src="home" @loaded="onLoaded"/>
+ <x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/>
+ <x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
+ <mk-user-list-timeline v-if="src.startsWith('list:')" ref="tl" :key="list.id" :list="list"/>
</div>
</main>
</mk-ui>
@@ -61,144 +44,58 @@
<script lang="ts">
import Vue from 'vue';
-import * as XDraggable from 'vuedraggable';
-import * as uuid from 'uuid';
import Progress from '../../../common/scripts/loading';
-import getNoteSummary from '../../../../../renderers/get-note-summary';
+import XTl from './home.timeline.vue';
export default Vue.extend({
components: {
- XDraggable
+ XTl
},
+
data() {
return {
- connection: null,
- connectionId: null,
- unreadCount: 0,
- showTl: true,
- widgets: [],
- customizing: false,
- widgetAdderSelected: null
+ src: 'home',
+ list: null,
+ lists: null,
+ showNav: false
};
},
- created() {
- if ((this as any).os.i.clientSettings.mobileHome == null) {
- Vue.set((this as any).os.i.clientSettings, 'mobileHome', [{
- name: 'calendar',
- id: 'a', data: {}
- }, {
- name: 'activity',
- id: 'b', data: {}
- }, {
- name: 'rss',
- id: 'c', data: {}
- }, {
- name: 'photo-stream',
- id: 'd', data: {}
- }, {
- name: 'donation',
- id: 'e', data: {}
- }, {
- name: 'nav',
- id: 'f', data: {}
- }, {
- name: 'version',
- id: 'g', data: {}
- }]);
- this.widgets = (this as any).os.i.clientSettings.mobileHome;
- this.saveHome();
- } else {
- this.widgets = (this as any).os.i.clientSettings.mobileHome;
+
+ watch: {
+ src() {
+ this.showNav = false;
+ },
+
+ showNav(v) {
+ if (v && this.lists === null) {
+ (this as any).api('users/lists/list').then(lists => {
+ this.lists = lists;
+ });
+ }
}
+ },
- this.$watch('os.i.clientSettings', i => {
- this.widgets = (this as any).os.i.clientSettings.mobileHome;
- }, {
- deep: true
- });
+ created() {
+ if ((this as any).os.i.followingCount == 0) {
+ this.src = 'local';
+ }
},
+
mounted() {
document.title = 'Misskey';
- document.documentElement.style.background = '#313a42';
-
- this.connection = (this as any).os.stream.getConnection();
- this.connectionId = (this as any).os.stream.use();
-
- this.connection.on('note', this.onStreamNote);
- this.connection.on('mobile_home_updated', this.onHomeUpdated);
- document.addEventListener('visibilitychange', this.onVisibilitychange, false);
Progress.start();
},
- beforeDestroy() {
- this.connection.off('note', this.onStreamNote);
- this.connection.off('mobile_home_updated', this.onHomeUpdated);
- (this as any).os.stream.dispose(this.connectionId);
- document.removeEventListener('visibilitychange', this.onVisibilitychange);
- },
+
methods: {
fn() {
(this as any).apis.post();
},
+
onLoaded() {
Progress.done();
},
- onStreamNote(note) {
- if (document.hidden && note.userId !== (this as any).os.i.id) {
- this.unreadCount++;
- document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`;
- }
- },
- onVisibilitychange() {
- if (!document.hidden) {
- this.unreadCount = 0;
- document.title = 'Misskey';
- }
- },
- onHomeUpdated(data) {
- if (data.home) {
- (this as any).os.i.clientSettings.mobileHome = data.home;
- this.widgets = data.home;
- } else {
- const w = (this as any).os.i.clientSettings.mobileHome.find(w => w.id == data.id);
- if (w != null) {
- w.data = data.data;
- this.$refs[w.id][0].preventSave = true;
- this.$refs[w.id][0].props = w.data;
- this.widgets = (this as any).os.i.clientSettings.mobileHome;
- }
- }
- },
- hint() {
- alert('ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。');
- },
- widgetFunc(id) {
- const w = this.$refs[id][0];
- if (w.func) w.func();
- },
- onWidgetSort() {
- this.saveHome();
- },
- addWidget() {
- const widget = {
- name: this.widgetAdderSelected,
- id: uuid(),
- data: {}
- };
- this.widgets.unshift(widget);
- this.saveHome();
- },
- removeWidget(widget) {
- this.widgets = this.widgets.filter(w => w.id != widget.id);
- this.saveHome();
- },
- saveHome() {
- (this as any).os.i.clientSettings.mobileHome = this.widgets;
- (this as any).api('i/update_mobile_home', {
- home: this.widgets
- });
- },
warp() {
}
@@ -207,53 +104,74 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-main
+@import '~const.styl'
- > .tl
- > .mk-timeline
- max-width 600px
+root(isDark)
+ > .nav
+ > .bg
+ position fixed
+ z-index 10000
+ top 0
+ left 0
+ width 100%
+ height 100%
+ background rgba(#000, 0.5)
+
+ > .body
+ position fixed
+ z-index 10001
+ top 56px
+ left 0
+ right 0
+ width 300px
margin 0 auto
- padding 8px
+ background isDark ? #272f3a : #fff
+ border-radius 8px
+ box-shadow 0 0 16px rgba(#000, 0.1)
- @media (min-width 500px)
- padding 16px
+ $balloon-size = 16px
- > .widgets
- margin 0 auto
- max-width 500px
+ &:after
+ content ""
+ display block
+ position absolute
+ top -($balloon-size * 2) + 1.5px
+ left s('calc(50% - %s)', $balloon-size)
+ border-top solid $balloon-size transparent
+ border-left solid $balloon-size transparent
+ border-right solid $balloon-size transparent
+ border-bottom solid $balloon-size isDark ? #272f3a : #fff
- @media (min-width 500px)
- padding 8px
+ > div
+ padding 8px 0
- > header
- padding 8px
- background #fff
+ > *
+ display block
+ padding 8px 16px
+ color isDark ? #cdd0d8 : #666
- .widget
- margin 8px
+ &[data-active]
+ color $theme-color-foreground
+ background $theme-color
- .customize-container
- margin 8px
- background #fff
+ &:not([data-active]):hover
+ background isDark ? #353e4a : #eee
- > header
- line-height 32px
- background #eee
+ > .tl
+ max-width 680px
+ margin 0 auto
+ padding 8px
- > .handle
- padding 0 8px
+ @media (min-width 500px)
+ padding 16px
- > .remove
- position absolute
- top 0
- right 0
- padding 0 8px
- line-height 32px
+ @media (min-width 600px)
+ padding 32px
- > div
- padding 8px
+main[data-darkmode]
+ root(true)
- > *
- pointer-events none
+main:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/mobile/views/pages/note.vue b/src/client/app/mobile/views/pages/note.vue
index c866be8a14..146d89d22b 100644
--- a/src/client/app/mobile/views/pages/note.vue
+++ b/src/client/app/mobile/views/pages/note.vue
@@ -2,11 +2,13 @@
<mk-ui>
<span slot="header">%fa:R sticky-note%%i18n:@title%</span>
<main v-if="!fetching">
- <a v-if="note.next" :href="note.next">%fa:angle-up%%i18n:@next%</a>
<div>
<mk-note-detail :note="note"/>
</div>
- <a v-if="note.prev" :href="note.prev">%fa:angle-down%%i18n:@prev%</a>
+ <footer>
+ <router-link v-if="note.prev" :to="note.prev">%fa:angle-left% %i18n:@prev%</router-link>
+ <router-link v-if="note.next" :to="note.next">%i18n:@next% %fa:angle-right%</router-link>
+ </footer>
</main>
</mk-ui>
</template>
@@ -30,7 +32,6 @@ export default Vue.extend({
},
mounted() {
document.title = 'Misskey';
- document.documentElement.style.background = '#313a42';
},
methods: {
fetch() {
@@ -53,33 +54,24 @@ export default Vue.extend({
<style lang="stylus" scoped>
main
text-align center
+ padding 8px
- > div
- margin 8px auto
- padding 0
- max-width 500px
- width calc(100% - 16px)
-
- @media (min-width 500px)
- margin 16px auto
- width calc(100% - 32px)
-
- > a
- display inline-block
+ @media (min-width 500px)
+ padding 16px
- &:first-child
- margin-top 8px
+ @media (min-width 600px)
+ padding 32px
- @media (min-width 500px)
- margin-top 16px
-
- &:last-child
- margin-bottom 8px
+ > div
+ margin 0 auto
+ padding 0
+ max-width 600px
- @media (min-width 500px)
- margin-bottom 16px
+ > footer
+ margin-top 16px
- > [data-fa]
- margin-right 4px
+ > a
+ display inline-block
+ margin 0 16px
</style>
diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue
index cd2b633676..d0c0fe9535 100644
--- a/src/client/app/mobile/views/pages/notifications.vue
+++ b/src/client/app/mobile/views/pages/notifications.vue
@@ -2,7 +2,10 @@
<mk-ui>
<span slot="header">%fa:R bell%%i18n:@notifications%</span>
<template slot="func"><button @click="fn">%fa:check%</button></template>
- <mk-notifications @fetched="onFetched"/>
+
+ <main>
+ <mk-notifications @fetched="onFetched"/>
+ </main>
</mk-ui>
</template>
@@ -13,7 +16,6 @@ import Progress from '../../../common/scripts/loading';
export default Vue.extend({
mounted() {
document.title = 'Misskey | %i18n:@notifications%';
- document.documentElement.style.background = '#313a42';
Progress.start();
},
@@ -30,3 +32,20 @@ export default Vue.extend({
}
});
</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+main
+ width 100%
+ max-width 680px
+ margin 0 auto
+ padding 8px
+
+ @media (min-width 500px)
+ padding 16px
+
+ @media (min-width 600px)
+ padding 32px
+
+</style>
diff --git a/src/client/app/mobile/views/pages/profile-setting.vue b/src/client/app/mobile/views/pages/profile-setting.vue
index 59da71c67d..7048cdef31 100644
--- a/src/client/app/mobile/views/pages/profile-setting.vue
+++ b/src/client/app/mobile/views/pages/profile-setting.vue
@@ -59,7 +59,6 @@ export default Vue.extend({
},
mounted() {
document.title = 'Misskey | %i18n:@title%';
- document.documentElement.style.background = '#313a42';
},
methods: {
setAvatar() {
@@ -137,7 +136,7 @@ export default Vue.extend({
.form
position relative
background #fff
- box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+ box-shadow 0 0 0 1px rgba(#000, 0.2)
border-radius 8px
&:before
@@ -146,7 +145,7 @@ export default Vue.extend({
position absolute
bottom -20px
left calc(50% - 10px)
- border-top solid 10px rgba(0, 0, 0, 0.2)
+ border-top solid 10px rgba(#000, 0.2)
border-right solid 10px transparent
border-bottom solid 10px transparent
border-left solid 10px transparent
diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue
index 31035f666a..f038a6f81f 100644
--- a/src/client/app/mobile/views/pages/search.vue
+++ b/src/client/app/mobile/views/pages/search.vue
@@ -39,7 +39,6 @@ export default Vue.extend({
},
mounted() {
document.title = `%i18n:@search%: ${this.q} | Misskey`;
- document.documentElement.style.background = '#313a42';
this.fetch();
},
@@ -85,7 +84,7 @@ export default Vue.extend({
width calc(100% - 16px)
background #fff
border-radius 8px
- box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+ box-shadow 0 0 0 1px rgba(#000, 0.2)
@media (min-width 500px)
margin 16px auto
diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue
index 741559ed0b..d730e4fcff 100644
--- a/src/client/app/mobile/views/pages/selectdrive.vue
+++ b/src/client/app/mobile/views/pages/selectdrive.vue
@@ -62,7 +62,7 @@ export default Vue.extend({
width 100%
z-index 1000
background #fff
- box-shadow 0 1px rgba(0, 0, 0, 0.1)
+ box-shadow 0 1px rgba(#000, 0.1)
> h1
margin 0
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index 8ae087749f..0e9c5ea962 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -34,7 +34,6 @@ export default Vue.extend({
},
mounted() {
document.title = 'Misskey | %i18n:@settings%';
- document.documentElement.style.background = '#313a42';
},
methods: {
signout() {
@@ -63,7 +62,7 @@ export default Vue.extend({
width calc(100% - 32px)
list-style none
background #fff
- border solid 1px rgba(0, 0, 0, 0.2)
+ border solid 1px rgba(#000, 0.2)
border-radius $radius
> li
@@ -71,7 +70,7 @@ export default Vue.extend({
border-bottom solid 1px #ddd
&:hover
- background rgba(0, 0, 0, 0.1)
+ background rgba(#000, 0.1)
&:first-child
border-top-left-radius $radius
diff --git a/src/client/app/mobile/views/pages/signup.vue b/src/client/app/mobile/views/pages/signup.vue
index 9dc07a4b86..b8245beb00 100644
--- a/src/client/app/mobile/views/pages/signup.vue
+++ b/src/client/app/mobile/views/pages/signup.vue
@@ -40,7 +40,7 @@ export default Vue.extend({
.form
background #fff
- border solid 1px rgba(0, 0, 0, 0.2)
+ border solid 1px rgba(#000, 0.2)
border-radius 8px
overflow hidden
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 04db482df2..27482dc215 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -1,14 +1,15 @@
<template>
<mk-ui>
- <span slot="header" v-if="!fetching">%fa:user% {{ user | userName }}</span>
- <main v-if="!fetching">
- <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>
+ <template slot="header" v-if="!fetching"><img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">{{ user | userName }}</template>
+ <main v-if="!fetching" :data-darkmode="_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>
- <div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"></div>
+ <div class="banner" :style="style"></div>
<div class="body">
<div class="top">
<a class="avatar">
- <img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/>
+ <img :src="user.avatarUrl" alt="avatar"/>
</a>
<mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
</div>
@@ -44,9 +45,9 @@
</header>
<nav>
<div class="nav-container">
- <a :data-is-active="page == 'home'" @click="page = 'home'">%i18n:@overview%</a>
- <a :data-is-active="page == 'notes'" @click="page = 'notes'">%i18n:@timeline%</a>
- <a :data-is-active="page == 'media'" @click="page = 'media'">%i18n:@media%</a>
+ <a :data-active="page == 'home'" @click="page = 'home'">%fa:home% %i18n:@overview%</a>
+ <a :data-active="page == 'notes'" @click="page = 'notes'">%fa:R comment-alt% %i18n:@timeline%</a>
+ <a :data-active="page == 'media'" @click="page = 'media'">%fa:image% %i18n:@media%</a>
</div>
</nav>
<div class="body">
@@ -79,6 +80,13 @@ export default Vue.extend({
computed: {
age(): number {
return age(this.user.profile.birthday);
+ },
+ style(): any {
+ if (this.user.bannerUrl == null) return {};
+ return {
+ backgroundColor: this.user.bannerColor ? `rgb(${ this.user.bannerColor.join(',') })` : null,
+ backgroundImage: `url(${ this.user.bannerUrl })`
+ };
}
},
watch: {
@@ -87,9 +95,6 @@ export default Vue.extend({
created() {
this.fetch();
},
- mounted() {
- document.documentElement.style.background = '#313a42';
- },
methods: {
fetch() {
Progress.start();
@@ -109,27 +114,38 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-main
+root(isDark)
+ $bg = isDark ? #22252f : #f7f7f7
+
+ > .is-suspended
> .is-remote
- padding 16px
- color #573c08
- background #fff0db
+ &.is-suspended
+ color #570808
+ background #ffdbdb
+
+ &.is-remote
+ color #573c08
+ background #fff0db
> p
margin 0 auto
- max-width 1024px
+ padding 14px
+ max-width 600px
+ font-size 14px
> a
font-weight bold
@media (max-width 500px)
+ padding 12px
font-size 12px
> header
+ background $bg
> .banner
padding-bottom 33.3%
- background-color #1b1b1b
+ background-color isDark ? #5f7273 : #cacaca
background-size cover
background-position center
@@ -156,13 +172,14 @@ main
left -2px
bottom -2px
width 100%
- border 3px solid #313a42
+ background $bg
+ border 3px solid $bg
border-radius 6px
@media (min-width 500px)
left -4px
bottom -4px
- border 4px solid #313a42
+ border 4px solid $bg
border-radius 12px
> .mk-follow-button
@@ -176,26 +193,26 @@ main
margin 0
line-height 22px
font-size 20px
- color #fff
+ color isDark ? #fff : #757c82
> .username
display inline-block
line-height 20px
font-size 16px
font-weight bold
- color #657786
+ color isDark ? #657786 : #969ea5
> .followed
margin-left 8px
padding 2px 4px
font-size 12px
- color #657786
- background #f8f8f8
+ color isDark ? #657786 : #fff
+ background isDark ? #f8f8f8 : #a7bec7
border-radius 4px
> .description
margin 8px 0
- color #fff
+ color isDark ? #fff : #757c82
> .info
margin 8px 0
@@ -203,14 +220,14 @@ main
> p
display inline
margin 0 16px 0 0
- color #a9b9c1
+ color isDark ? #a9b9c1 : #90989c
> i
margin-right 4px
> .status
> a
- color #657786
+ color isDark ? #657786 : #818a92
&:not(:last-child)
margin-right 16px
@@ -218,7 +235,7 @@ main
> b
margin-right 4px
font-size 16px
- color #fff
+ color isDark ? #fff : #787e86
> i
font-size 14px
@@ -226,9 +243,9 @@ main
> nav
position -webkit-sticky
position sticky
- top 48px
- box-shadow 0 4px 4px rgba(0, 0, 0, 0.3)
- background-color #313a42
+ top 47px
+ box-shadow 0 4px 4px isDark ? rgba(#000, 0.3) : rgba(#000, 0.07)
+ background-color $bg
z-index 1
> .nav-container
@@ -241,21 +258,36 @@ main
display block
flex 1 1
text-align center
- line-height 52px
- font-size 14px
+ line-height 48px
+ font-size 12px
text-decoration none
- color #657786
+ color isDark ? #657786 : #9ca1a5
border-bottom solid 2px transparent
- &[data-is-active]
+ @media (min-width 400px)
+ line-height 52px
+ font-size 14px
+
+ &[data-active]
font-weight bold
color $theme-color
border-color $theme-color
> .body
+ max-width 680px
+ margin 0 auto
padding 8px
@media (min-width 500px)
padding 16px
+ @media (min-width 600px)
+ padding 32px
+
+main[data-darkmode]
+ root(true)
+
+main:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue
index 4ba2ffd1df..d02daf5027 100644
--- a/src/client/app/mobile/views/pages/user/home.vue
+++ b/src/client/app/mobile/views/pages/user/home.vue
@@ -54,30 +54,39 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.root.home
+root(isDark)
max-width 600px
margin 0 auto
> .mk-note-detail
margin 0 0 8px 0
+ @media (min-width 500px)
+ margin 0 0 16px 0
+
> section
- background #eee
+ background isDark ? #21242f : #eee
border-radius 8px
- box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+ box-shadow 0 4px 16px rgba(#000, 0.1)
&:not(:last-child)
margin-bottom 8px
+ @media (min-width 500px)
+ margin-bottom 16px
+
> h2
margin 0
padding 8px 10px
font-size 15px
font-weight normal
- color #465258
- background #fff
+ color isDark ? #b8c5cc : #465258
+ background isDark ? #282c37 : #fff
border-radius 8px 8px 0 0
+ @media (min-width 500px)
+ padding 10px 16px
+
> i
margin-right 6px
@@ -89,6 +98,12 @@ export default Vue.extend({
display block
margin 16px
text-align center
- color #cad2da
+ color isDark ? #cad2da : #929aa0
+
+.root.home[data-darkmode]
+ root(true)
+
+.root.home:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
index 27baf8bee4..64cfa5a46c 100644
--- a/src/client/app/mobile/views/pages/welcome.vue
+++ b/src/client/app/mobile/views/pages/welcome.vue
@@ -1,33 +1,33 @@
<template>
<div class="welcome">
- <h1><b>Misskey</b>へようこそ</h1>
- <p>Twitter風ミニブログSNS、Misskeyへようこそ。共有したいことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<br><a href="/signup">アカウントを作成する</a></p>
- <div class="form">
- <p>%fa:lock% ログイン</p>
- <div>
- <form @submit.prevent="onSubmit">
- <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/>
- <input v-model="password" type="password" placeholder="パスワード" required/>
- <input v-if="user && user.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/>
- <button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button>
- </form>
+ <div>
+ <h1><b>Misskey</b>へようこそ</h1>
+ <p>Twitter風ミニブログSNS、Misskeyへようこそ。共有したいことを投稿したり、タイムラインでみんなの投稿を読むこともできます。<br><a href="/signup">アカウントを作成する</a></p>
+ <div class="form">
+ <p>%fa:lock% ログイン</p>
<div>
- <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a>
+ <form @submit.prevent="onSubmit">
+ <input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" placeholder="ユーザー名" autofocus required @change="onUsernameChange"/>
+ <input v-model="password" type="password" placeholder="パスワード" required/>
+ <input v-if="user && user.twoFactorEnabled" v-model="token" type="number" placeholder="トークン" required/>
+ <button type="submit" :disabled="signing">{{ signing ? 'ログインしています' : 'ログイン' }}</button>
+ </form>
+ <div>
+ <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a>
+ </div>
</div>
</div>
+ <div class="tl">
+ <p>%fa:comments R% タイムラインを見てみる</p>
+ <mk-welcome-timeline/>
+ </div>
+ <div class="users">
+ <mk-avatar class="avatar" v-for="user in users" :key="user.id" :user="user"/>
+ </div>
+ <footer>
+ <small>{{ copyright }}</small>
+ </footer>
</div>
- <div class="tl">
- <p>%fa:comments R% タイムラインを見てみる</p>
- <mk-welcome-timeline/>
- </div>
- <div class="users">
- <router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${user.username}`">
- <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
- </router-link>
- </div>
- <footer>
- <small>{{ copyright }}</small>
- </footer>
</div>
</template>
@@ -84,123 +84,116 @@ export default Vue.extend({
<style lang="stylus" scoped>
.welcome
- padding 16px
- margin 0 auto
- max-width 500px
-
- h1
- margin 0
- padding 8px
- font-size 1.5em
- font-weight normal
- color #cacac3
-
- & + p
- margin 0 0 16px 0
- padding 0 8px 0 8px
- color #949fa9
+ background linear-gradient(to bottom, #1e1d65, #bd6659)
- .form
- margin-bottom 16px
- background #fff
- border solid 1px rgba(0, 0, 0, 0.2)
- border-radius 8px
- overflow hidden
+ > div
+ padding 16px
+ margin 0 auto
+ max-width 500px
- > p
+ h1
margin 0
- padding 12px 20px
- color #555
- background #f5f5f5
- border-bottom solid 1px #ddd
+ padding 8px
+ font-size 1.5em
+ font-weight normal
+ color #cacac3
- > div
+ & + p
+ margin 0 0 16px 0
+ padding 0 8px 0 8px
+ color #949fa9
- > form
- padding 16px
+ .form
+ margin-bottom 16px
+ background #fff
+ border solid 1px rgba(#000, 0.2)
+ border-radius 8px
+ overflow hidden
+
+ > p
+ margin 0
+ padding 12px 20px
+ color #555
+ background #f5f5f5
border-bottom solid 1px #ddd
- input
- display block
- padding 12px
- margin 0 0 16px 0
- width 100%
- font-size 1em
- color rgba(0, 0, 0, 0.7)
- background #fff
- outline none
- border solid 1px #ddd
- border-radius 4px
+ > div
+
+ > form
+ padding 16px
+ border-bottom solid 1px #ddd
- button
- display block
- width 100%
- padding 10px
- margin 0
- color #333
- font-size 1em
- text-align center
- text-decoration none
- text-shadow 0 1px 0 rgba(255, 255, 255, 0.9)
- background-image linear-gradient(#fafafa, #eaeaea)
- border 1px solid #ddd
- border-bottom-color #cecece
- border-radius 4px
+ input
+ display block
+ padding 12px
+ margin 0 0 16px 0
+ width 100%
+ font-size 1em
+ color rgba(#000, 0.7)
+ background #fff
+ outline none
+ border solid 1px #ddd
+ border-radius 4px
- &:active
- background-color #767676
- background-image none
- border-color #444
- box-shadow 0 1px 3px rgba(0, 0, 0, 0.075), inset 0 0 5px rgba(0, 0, 0, 0.2)
+ button
+ display block
+ width 100%
+ padding 10px
+ margin 0
+ color #333
+ font-size 1em
+ text-align center
+ text-decoration none
+ text-shadow 0 1px 0 rgba(255, 255, 255, 0.9)
+ background-image linear-gradient(#fafafa, #eaeaea)
+ border 1px solid #ddd
+ border-bottom-color #cecece
+ border-radius 4px
- > div
- padding 16px
- text-align center
+ &:active
+ background-color #767676
+ background-image none
+ border-color #444
+ box-shadow 0 1px 3px rgba(#000, 0.075), inset 0 0 5px rgba(#000, 0.2)
- > .tl
- background #fff
- border solid 1px rgba(0, 0, 0, 0.2)
- border-radius 8px
- overflow hidden
+ > div
+ padding 16px
+ text-align center
- > p
- margin 0
- padding 12px 20px
- color #555
- background #f5f5f5
- border-bottom solid 1px #ddd
+ > .tl
+ background #fff
+ border solid 1px rgba(#000, 0.2)
+ border-radius 8px
+ overflow hidden
- > .mk-welcome-timeline
- max-height 300px
- overflow auto
+ > p
+ margin 0
+ padding 12px 20px
+ color #555
+ background #f5f5f5
+ border-bottom solid 1px #ddd
- > .users
- margin 12px 0 0 0
+ > .mk-welcome-timeline
+ max-height 300px
+ overflow auto
- > *
- display inline-block
- margin 4px
+ > .users
+ margin 12px 0 0 0
> *
display inline-block
+ margin 4px
width 38px
height 38px
- vertical-align top
border-radius 6px
- > footer
- text-align center
- color #fff
-
- > small
- display block
- margin 16px 0 0 0
- opacity 0.7
+ > footer
+ text-align center
+ color #fff
-</style>
+ > small
+ display block
+ margin 16px 0 0 0
+ opacity 0.7
-<style lang="stylus">
-html
-body
- background linear-gradient(to bottom, #1e1d65, #bd6659)
</style>
diff --git a/src/client/app/mobile/views/widgets/activity.vue b/src/client/app/mobile/views/widgets/activity.vue
index 48dcafb3ed..7763be41f5 100644
--- a/src/client/app/mobile/views/widgets/activity.vue
+++ b/src/client/app/mobile/views/widgets/activity.vue
@@ -21,6 +21,7 @@ export default define({
methods: {
func() {
this.props.compact = !this.props.compact;
+ this.save();
}
}
});
diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue
index 502f886ceb..59c1ec7c0e 100644
--- a/src/client/app/mobile/views/widgets/profile.vue
+++ b/src/client/app/mobile/views/widgets/profile.vue
@@ -34,7 +34,7 @@ export default define({
display block
width 100%
height 100%
- background rgba(0, 0, 0, 0.5)
+ background rgba(#000, 0.5)
.avatar
display block
@@ -47,7 +47,7 @@ export default define({
left ((100px - 58px) / 2)
border none
border-radius 100%
- box-shadow 0 0 16px rgba(0, 0, 0, 0.5)
+ box-shadow 0 0 16px rgba(#000, 0.5)
.name
display block
@@ -58,6 +58,6 @@ export default define({
line-height 100px
color #fff
font-weight bold
- text-shadow 0 0 8px rgba(0, 0, 0, 0.5)
+ text-shadow 0 0 8px rgba(#000, 0.5)
</style>
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
new file mode 100644
index 0000000000..0bdfdef6a0
--- /dev/null
+++ b/src/client/app/store.ts
@@ -0,0 +1,92 @@
+import Vuex from 'vuex';
+import MiOS from './mios';
+
+const defaultSettings = {
+ home: [],
+ fetchOnScroll: true,
+ showMaps: true,
+ showPostFormOnTopOfTl: false,
+ circleIcons: true,
+ gradientWindowHeader: false,
+ showReplyTarget: true,
+ showMyRenotes: true,
+ showRenotedMyNotes: true
+};
+
+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));
+ }
+ });
+ }],
+
+ state: {
+ uiHeaderHeight: 0
+ },
+
+ mutations: {
+ setUiHeaderHeight(state, height) {
+ state.uiHeaderHeight = height;
+ }
+ },
+
+ modules: {
+ settings: {
+ namespaced: true,
+
+ state: {
+ data: defaultSettings
+ },
+
+ mutations: {
+ set(state, x: { key: string; value: any }) {
+ state.data[x.key] = x.value;
+ },
+
+ setHome(state, data) {
+ state.data.home = data;
+ },
+
+ setHomeWidget(state, x) {
+ const w = state.data.home.find(w => w.id == x.id);
+ if (w) {
+ w.data = x.data;
+ }
+ },
+
+ addHomeWidget(state, widget) {
+ state.data.home.unshift(widget);
+ }
+ },
+
+ actions: {
+ merge(ctx, settings) {
+ Object.entries(settings).forEach(([key, value]) => {
+ ctx.commit('set', { key, value });
+ });
+ },
+
+ set(ctx, x) {
+ ctx.commit('set', x);
+
+ if (os.isSignedIn) {
+ os.api('i/update_client_setting', {
+ name: x.key,
+ value: x.value
+ });
+ }
+ },
+
+ addHomeWidget(ctx, widget) {
+ ctx.commit('addHomeWidget', widget);
+
+ os.api('i/update_home', {
+ home: ctx.state.data.home
+ });
+ }
+ }
+ }
+ }
+});
diff --git a/src/client/docs/api/endpoints/view.pug b/src/client/docs/api/endpoints/view.pug
index d271a5517a..f8795c8442 100644
--- a/src/client/docs/api/endpoints/view.pug
+++ b/src/client/docs/api/endpoints/view.pug
@@ -2,7 +2,7 @@ extends ../../layout.pug
include ../mixins
block meta
- link(rel="stylesheet" href="/assets/api/endpoints/style.css")
+ link(rel="stylesheet" href="/docs/assets/api/endpoints/style.css")
block main
h1= endpoint
diff --git a/src/client/docs/api/entities/note.yaml b/src/client/docs/api/entities/note.yaml
index 718d331d13..6fd26543bb 100644
--- a/src/client/docs/api/entities/note.yaml
+++ b/src/client/docs/api/entities/note.yaml
@@ -29,12 +29,6 @@ props:
desc:
ja: "投稿の本文 (ローカルの場合Markdown風のフォーマット)"
en: "The text of this note (in Markdown like format if local)"
- - name: "textHtml"
- type: "string"
- optional: true
- desc:
- ja: "投稿の本文 (HTML) (投稿時は無視)"
- en: "The text of this note (in HTML. Ignored when posting.)"
- name: "mediaIds"
type: "id(DriveFile)[]"
optional: true
diff --git a/src/client/docs/api/entities/post.yaml b/src/client/docs/api/entities/post.yaml
index 718d331d13..6fd26543bb 100644
--- a/src/client/docs/api/entities/post.yaml
+++ b/src/client/docs/api/entities/post.yaml
@@ -29,12 +29,6 @@ props:
desc:
ja: "投稿の本文 (ローカルの場合Markdown風のフォーマット)"
en: "The text of this note (in Markdown like format if local)"
- - name: "textHtml"
- type: "string"
- optional: true
- desc:
- ja: "投稿の本文 (HTML) (投稿時は無視)"
- en: "The text of this note (in HTML. Ignored when posting.)"
- name: "mediaIds"
type: "id(DriveFile)[]"
optional: true
diff --git a/src/client/docs/api/entities/view.pug b/src/client/docs/api/entities/view.pug
index 2156463dc7..ac938151a7 100644
--- a/src/client/docs/api/entities/view.pug
+++ b/src/client/docs/api/entities/view.pug
@@ -2,7 +2,7 @@ extends ../../layout.pug
include ../mixins
block meta
- link(rel="stylesheet" href="/assets/api/entities/style.css")
+ link(rel="stylesheet" href="/docs/assets/api/entities/style.css")
block main
h1= name
diff --git a/src/client/docs/api/mixins.pug b/src/client/docs/api/mixins.pug
index 686bf6a2b6..913135a85f 100644
--- a/src/client/docs/api/mixins.pug
+++ b/src/client/docs/api/mixins.pug
@@ -14,13 +14,13 @@ mixin propTable(props)
if prop.kind == 'id'
if prop.entity
| (
- a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
+ a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
| ID)
else
| (ID)
else if prop.kind == 'entity'
| (
- a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
+ a(href=`/docs/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
| )
else if prop.kind == 'object'
if prop.def
diff --git a/src/client/docs/follow.ja.pug b/src/client/docs/follow.ja.pug
new file mode 100644
index 0000000000..f0e83bc8fd
--- /dev/null
+++ b/src/client/docs/follow.ja.pug
@@ -0,0 +1,9 @@
+h1 フォロー
+p ユーザーをフォローすると、タイムラインにそのユーザーの投稿が表示されるようになります。ただし、他のユーザーに対する返信は含まれません。
+p ユーザーをフォローするには、ユーザーページの「フォロー」ボタンをクリックします。フォローを解除するには、もう一度クリックします。
+
+section
+ h2 ストーキング
+ p ユーザーをフォローしている状態では、さらに「ストーキング」モードをオンにすることができます。ストーキングを行うと、タイムラインにそのユーザーの全ての投稿が表示されるようになります。つまり、他のユーザーに対する返信も含まれることになります。
+ p ストーキングするには、ユーザーページの「ストークする」をクリックします。ストーキングをやめるには、もう一度クリックします。
+ p ストーキングしていることは相手に通知されません。