summaryrefslogtreecommitdiff
path: root/src/client/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/app')
-rw-r--r--src/client/app/app.styl4
-rw-r--r--src/client/app/auth/views/index.vue2
-rw-r--r--src/client/app/boot.js12
-rw-r--r--src/client/app/common/scripts/check-for-update.ts2
-rw-r--r--src/client/app/common/scripts/gcd.ts2
-rw-r--r--src/client/app/common/scripts/parse-search-query.ts53
-rw-r--r--src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts6
-rw-r--r--src/client/app/common/scripts/streaming/hashtag.ts13
-rw-r--r--src/client/app/common/scripts/streaming/local-timeline.ts4
-rw-r--r--src/client/app/common/scripts/streaming/stream-manager.ts3
-rw-r--r--src/client/app/common/scripts/streaming/stream.ts4
-rw-r--r--src/client/app/common/views/components/acct.vue12
-rw-r--r--src/client/app/common/views/components/autocomplete.vue4
-rw-r--r--src/client/app/common/views/components/avatar.vue21
-rw-r--r--src/client/app/common/views/components/connect-failed.troubleshooter.vue2
-rw-r--r--src/client/app/common/views/components/cw-button.vue44
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.game.vue24
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.index.vue1
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.room.vue7
-rw-r--r--src/client/app/common/views/components/index.ts6
-rw-r--r--src/client/app/common/views/components/media-banner.vue90
-rw-r--r--src/client/app/common/views/components/media-list.vue121
-rw-r--r--src/client/app/common/views/components/menu.vue24
-rw-r--r--src/client/app/common/views/components/messaging-room.vue24
-rw-r--r--src/client/app/common/views/components/misskey-flavored-markdown.ts70
-rw-r--r--src/client/app/common/views/components/note-menu.vue32
-rw-r--r--src/client/app/common/views/components/poll-editor.vue3
-rw-r--r--src/client/app/common/views/components/poll.vue3
-rw-r--r--src/client/app/common/views/components/reaction-icon.vue22
-rw-r--r--src/client/app/common/views/components/reaction-picker.vue4
-rw-r--r--src/client/app/common/views/components/signin.vue2
-rw-r--r--src/client/app/common/views/components/tag-cloud.vue90
-rw-r--r--src/client/app/common/views/components/trends.chart.vue (renamed from src/client/app/common/views/widgets/hashtags.chart.vue)0
-rw-r--r--src/client/app/common/views/components/trends.vue103
-rw-r--r--src/client/app/common/views/components/ui/card.vue27
-rw-r--r--src/client/app/common/views/components/ui/radio.vue2
-rw-r--r--src/client/app/common/views/components/ui/switch.vue7
-rw-r--r--src/client/app/common/views/components/url-preview.vue35
-rw-r--r--src/client/app/common/views/components/url.vue9
-rw-r--r--src/client/app/common/views/components/visibility-chooser.vue10
-rw-r--r--src/client/app/common/views/components/welcome-timeline.vue161
-rw-r--r--src/client/app/common/views/directives/autocomplete.ts6
-rw-r--r--src/client/app/common/views/filters/note.ts2
-rw-r--r--src/client/app/common/views/filters/user.ts2
-rw-r--r--src/client/app/common/views/pages/follow.vue5
-rw-r--r--src/client/app/common/views/widgets/analog-clock.vue9
-rw-r--r--src/client/app/common/views/widgets/broadcast.vue43
-rw-r--r--src/client/app/common/views/widgets/hashtags.vue94
-rw-r--r--src/client/app/config.ts2
-rw-r--r--src/client/app/desktop/api/update-avatar.ts2
-rw-r--r--src/client/app/desktop/api/update-banner.ts2
-rw-r--r--src/client/app/desktop/script.ts1
-rw-r--r--src/client/app/desktop/views/components/charts.vue107
-rw-r--r--src/client/app/desktop/views/components/context-menu.vue2
-rw-r--r--src/client/app/desktop/views/components/dialog.vue2
-rw-r--r--src/client/app/desktop/views/components/drive.folder.vue2
-rw-r--r--src/client/app/desktop/views/components/drive.vue4
-rw-r--r--src/client/app/desktop/views/components/follow-button.vue8
-rw-r--r--src/client/app/desktop/views/components/friends-maker.vue2
-rw-r--r--src/client/app/desktop/views/components/media-image-dialog.vue2
-rw-r--r--src/client/app/desktop/views/components/media-image.vue15
-rw-r--r--src/client/app/desktop/views/components/media-video-dialog.vue2
-rw-r--r--src/client/app/desktop/views/components/media-video.vue11
-rw-r--r--src/client/app/desktop/views/components/note-detail.vue100
-rw-r--r--src/client/app/desktop/views/components/note-preview.vue37
-rw-r--r--src/client/app/desktop/views/components/notes.note.sub.vue50
-rw-r--r--src/client/app/desktop/views/components/notes.note.vue39
-rw-r--r--src/client/app/desktop/views/components/notes.vue8
-rw-r--r--src/client/app/desktop/views/components/notifications.vue6
-rw-r--r--src/client/app/desktop/views/components/post-form-window.vue10
-rw-r--r--src/client/app/desktop/views/components/post-form.vue50
-rw-r--r--src/client/app/desktop/views/components/renote-form.vue4
-rw-r--r--src/client/app/desktop/views/components/settings-window.vue8
-rw-r--r--src/client/app/desktop/views/components/settings.drive.vue1
-rw-r--r--src/client/app/desktop/views/components/settings.profile.vue9
-rw-r--r--src/client/app/desktop/views/components/settings.tags.vue65
-rw-r--r--src/client/app/desktop/views/components/settings.vue317
-rw-r--r--src/client/app/desktop/views/components/sub-note-content.vue6
-rw-r--r--src/client/app/desktop/views/components/taskmanager.vue219
-rw-r--r--src/client/app/desktop/views/components/timeline.core.vue76
-rw-r--r--src/client/app/desktop/views/components/timeline.vue132
-rw-r--r--src/client/app/desktop/views/components/ui-notification.vue2
-rw-r--r--src/client/app/desktop/views/components/ui.header.vue19
-rw-r--r--src/client/app/desktop/views/components/user-preview.vue2
-rw-r--r--src/client/app/desktop/views/components/users-list.item.vue126
-rw-r--r--src/client/app/desktop/views/components/users-list.vue26
-rw-r--r--src/client/app/desktop/views/components/window.vue4
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.announcements.vue41
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.dashboard.vue35
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.hashtags.vue41
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.vue13
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.column-core.vue6
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.column.vue38
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue117
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.list-tl.vue6
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.mentions-column.vue38
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.mentions.vue93
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.note.vue21
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.notes.vue2
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.notifications.vue6
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.tl-column.vue7
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.tl.vue6
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.vue24
-rw-r--r--src/client/app/desktop/views/pages/drive.vue2
-rw-r--r--src/client/app/desktop/views/pages/games/reversi.vue4
-rw-r--r--src/client/app/desktop/views/pages/messaging-room.vue2
-rw-r--r--src/client/app/desktop/views/pages/stats/stats.vue5
-rw-r--r--src/client/app/desktop/views/pages/user/user.followers-you-know.vue14
-rw-r--r--src/client/app/desktop/views/pages/user/user.friends.vue7
-rw-r--r--src/client/app/desktop/views/pages/user/user.header.vue2
-rw-r--r--src/client/app/desktop/views/pages/user/user.photos.vue13
-rw-r--r--src/client/app/desktop/views/pages/user/user.timeline.vue4
-rw-r--r--src/client/app/desktop/views/pages/welcome.vue571
-rw-r--r--src/client/app/desktop/views/widgets/trends.vue2
-rw-r--r--src/client/app/init.ts25
-rw-r--r--src/client/app/mios.ts9
-rw-r--r--src/client/app/mobile/api/post.ts7
-rw-r--r--src/client/app/mobile/script.ts1
-rw-r--r--src/client/app/mobile/views/components/dialog.vue2
-rw-r--r--src/client/app/mobile/views/components/drive-file-chooser.vue27
-rw-r--r--src/client/app/mobile/views/components/drive-folder-chooser.vue4
-rw-r--r--src/client/app/mobile/views/components/drive.file-detail.vue51
-rw-r--r--src/client/app/mobile/views/components/drive.file.vue23
-rw-r--r--src/client/app/mobile/views/components/drive.folder.vue12
-rw-r--r--src/client/app/mobile/views/components/drive.vue35
-rw-r--r--src/client/app/mobile/views/components/follow-button.vue4
-rw-r--r--src/client/app/mobile/views/components/friends-maker.vue2
-rw-r--r--src/client/app/mobile/views/components/media-image.vue13
-rw-r--r--src/client/app/mobile/views/components/media-video.vue15
-rw-r--r--src/client/app/mobile/views/components/note-detail.vue112
-rw-r--r--src/client/app/mobile/views/components/note-preview.vue43
-rw-r--r--src/client/app/mobile/views/components/note.sub.vue43
-rw-r--r--src/client/app/mobile/views/components/note.vue38
-rw-r--r--src/client/app/mobile/views/components/notes.vue8
-rw-r--r--src/client/app/mobile/views/components/notifications.vue6
-rw-r--r--src/client/app/mobile/views/components/notify.vue46
-rw-r--r--src/client/app/mobile/views/components/post-form-dialog.vue126
-rw-r--r--src/client/app/mobile/views/components/post-form.vue50
-rw-r--r--src/client/app/mobile/views/components/sub-note-content.vue6
-rw-r--r--src/client/app/mobile/views/components/ui.header.vue18
-rw-r--r--src/client/app/mobile/views/components/ui.nav.vue35
-rw-r--r--src/client/app/mobile/views/components/ui.vue7
-rw-r--r--src/client/app/mobile/views/components/user-timeline.vue4
-rw-r--r--src/client/app/mobile/views/pages/drive.vue6
-rw-r--r--src/client/app/mobile/views/pages/followers.vue2
-rw-r--r--src/client/app/mobile/views/pages/following.vue2
-rw-r--r--src/client/app/mobile/views/pages/games/reversi.vue4
-rw-r--r--src/client/app/mobile/views/pages/home.timeline.vue76
-rw-r--r--src/client/app/mobile/views/pages/home.vue31
-rw-r--r--src/client/app/mobile/views/pages/selectdrive.vue2
-rw-r--r--src/client/app/mobile/views/pages/settings.vue279
-rw-r--r--src/client/app/mobile/views/pages/settings/settings.profile.vue101
-rw-r--r--src/client/app/mobile/views/pages/user-lists.vue2
-rw-r--r--src/client/app/mobile/views/pages/user.vue4
-rw-r--r--src/client/app/mobile/views/pages/user/home.photos.vue2
-rw-r--r--src/client/app/mobile/views/pages/welcome.vue158
-rw-r--r--src/client/app/store.ts17
-rw-r--r--src/client/app/sw.js4
158 files changed, 3508 insertions, 1780 deletions
diff --git a/src/client/app/app.styl b/src/client/app/app.styl
index 431b9daa65..3911f83a61 100644
--- a/src/client/app/app.styl
+++ b/src/client/app/app.styl
@@ -6,6 +6,10 @@ html
&, *
cursor progress !important
+html
+ // iOSのため
+ overflow auto
+
body
overflow-wrap break-word
diff --git a/src/client/app/auth/views/index.vue b/src/client/app/auth/views/index.vue
index 609e758994..ba7df911e5 100644
--- a/src/client/app/auth/views/index.vue
+++ b/src/client/app/auth/views/index.vue
@@ -80,7 +80,7 @@ export default Vue.extend({
accepted() {
this.state = 'accepted';
if (this.session.app.callbackUrl) {
- location.href = this.session.app.callbackUrl + '?token=' + this.session.token;
+ location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
}
}
}
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 54397c98c6..25aa26dd19 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -18,6 +18,8 @@
return;
}
+ const langs = LANGS;
+
//#region Load settings
let settings = null;
const vuex = localStorage.getItem('vuex');
@@ -40,10 +42,10 @@
//#region Detect the user language
let lang = null;
- if (LANGS.includes(navigator.language)) {
+ if (langs.includes(navigator.language)) {
lang = navigator.language;
} else {
- lang = LANGS.find(x => x.split('-')[0] == navigator.language);
+ lang = langs.find(x => x.split('-')[0] == navigator.language);
if (lang == null) {
// Fallback
@@ -52,7 +54,7 @@
}
if (settings && settings.device.lang &&
- LANGS.includes(settings.device.lang)) {
+ langs.includes(settings.device.lang)) {
lang = settings.device.lang;
}
//#endregion
@@ -94,7 +96,7 @@
// Get salt query
const salt = localStorage.getItem('salt')
- ? '?salt=' + localStorage.getItem('salt')
+ ? `?salt=${localStorage.getItem('salt')}`
: '';
// Load an app script
@@ -140,7 +142,7 @@
// Random
localStorage.setItem('salt', Math.random().toString());
- // Clear cache (serive worker)
+ // Clear cache (service worker)
try {
navigator.serviceWorker.controller.postMessage('clear');
diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts
index 4445eefc39..91b165b45d 100644
--- a/src/client/app/common/scripts/check-for-update.ts
+++ b/src/client/app/common/scripts/check-for-update.ts
@@ -9,7 +9,7 @@ export default async function(mios: MiOS, force = false, silent = false) {
localStorage.setItem('should-refresh', 'true');
localStorage.setItem('v', newer);
- // Clear cache (serive worker)
+ // Clear cache (service worker)
try {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage('clear');
diff --git a/src/client/app/common/scripts/gcd.ts b/src/client/app/common/scripts/gcd.ts
deleted file mode 100644
index 9a19f9da66..0000000000
--- a/src/client/app/common/scripts/gcd.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-const gcd = (a, b) => !b ? a : gcd(b, a % b);
-export default gcd;
diff --git a/src/client/app/common/scripts/parse-search-query.ts b/src/client/app/common/scripts/parse-search-query.ts
deleted file mode 100644
index 5f6ae3320a..0000000000
--- a/src/client/app/common/scripts/parse-search-query.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-export default function(qs: string) {
- const q = {
- text: ''
- };
-
- qs.split(' ').forEach(x => {
- if (/^([a-z_]+?):(.+?)$/.test(x)) {
- const [key, value] = x.split(':');
- switch (key) {
- case 'user':
- q['includeUserUsernames'] = value.split(',');
- break;
- case 'exclude_user':
- q['excludeUserUsernames'] = value.split(',');
- break;
- case 'follow':
- q['following'] = value == 'null' ? null : value == 'true';
- break;
- case 'reply':
- q['reply'] = value == 'null' ? null : value == 'true';
- break;
- case 'renote':
- q['renote'] = value == 'null' ? null : value == 'true';
- break;
- case 'media':
- q['media'] = value == 'null' ? null : value == 'true';
- break;
- case 'poll':
- q['poll'] = value == 'null' ? null : value == 'true';
- break;
- case 'until':
- case 'since':
- // YYYY-MM-DD
- if (/^[0-9]+\-[0-9]+\-[0-9]+$/) {
- const [yyyy, mm, dd] = value.split('-');
- q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime();
- }
- break;
- default:
- q[key] = value;
- break;
- }
- } else {
- q.text += x + ' ';
- }
- });
-
- if (q.text) {
- q.text = q.text.trim();
- }
-
- return q;
-}
diff --git a/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts b/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts
index e6b02fcfdb..adfa75ff3b 100644
--- a/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts
+++ b/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts
@@ -3,8 +3,10 @@ import MiOS from '../../../../../mios';
export class ReversiGameStream extends Stream {
constructor(os: MiOS, me, game) {
- super(os, 'games/reversi-game', {
- i: me ? me.token : null,
+ super(os, 'games/reversi-game', me ? {
+ i: me.token,
+ game: game.id
+ } : {
game: game.id
});
}
diff --git a/src/client/app/common/scripts/streaming/hashtag.ts b/src/client/app/common/scripts/streaming/hashtag.ts
new file mode 100644
index 0000000000..276b8f8d3d
--- /dev/null
+++ b/src/client/app/common/scripts/streaming/hashtag.ts
@@ -0,0 +1,13 @@
+import Stream from './stream';
+import MiOS from '../../../mios';
+
+export class HashtagStream extends Stream {
+ constructor(os: MiOS, me, q) {
+ super(os, 'hashtag', me ? {
+ i: me.token,
+ q: JSON.stringify(q)
+ } : {
+ q: JSON.stringify(q)
+ });
+ }
+}
diff --git a/src/client/app/common/scripts/streaming/local-timeline.ts b/src/client/app/common/scripts/streaming/local-timeline.ts
index 2834262bdc..41c36aa14c 100644
--- a/src/client/app/common/scripts/streaming/local-timeline.ts
+++ b/src/client/app/common/scripts/streaming/local-timeline.ts
@@ -7,9 +7,9 @@ import MiOS from '../../../mios';
*/
export class LocalTimelineStream extends Stream {
constructor(os: MiOS, me) {
- super(os, 'local-timeline', {
+ super(os, 'local-timeline', me ? {
i: me.token
- });
+ } : {});
}
}
diff --git a/src/client/app/common/scripts/streaming/stream-manager.ts b/src/client/app/common/scripts/streaming/stream-manager.ts
index 568b8b0372..8dd06f67d3 100644
--- a/src/client/app/common/scripts/streaming/stream-manager.ts
+++ b/src/client/app/common/scripts/streaming/stream-manager.ts
@@ -1,6 +1,7 @@
import { EventEmitter } from 'eventemitter3';
import * as uuid from 'uuid';
import Connection from './stream';
+import { erase } from '../../../../../prelude/array';
/**
* ストリーム接続を管理するクラス
@@ -89,7 +90,7 @@ export default abstract class StreamManager<T extends Connection> extends EventE
* @param userId use で発行したユーザーID
*/
public dispose(userId) {
- this.users = this.users.filter(id => id != userId);
+ this.users = erase(userId, this.users);
this._connection.user = `Managed (${ this.users.length })`;
diff --git a/src/client/app/common/scripts/streaming/stream.ts b/src/client/app/common/scripts/streaming/stream.ts
index fefa8e5ced..4ab78f1190 100644
--- a/src/client/app/common/scripts/streaming/stream.ts
+++ b/src/client/app/common/scripts/streaming/stream.ts
@@ -44,11 +44,11 @@ export default class Connection extends EventEmitter {
const query = params
? Object.keys(params)
- .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
+ .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
.join('&')
: null;
- this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? '?' + query : ''}`);
+ this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? `?${query}` : ''}`);
this.socket.addEventListener('open', this.onOpen);
this.socket.addEventListener('close', this.onClose);
this.socket.addEventListener('message', this.onMessage);
diff --git a/src/client/app/common/views/components/acct.vue b/src/client/app/common/views/components/acct.vue
index 1ad222afdd..542fbb4296 100644
--- a/src/client/app/common/views/components/acct.vue
+++ b/src/client/app/common/views/components/acct.vue
@@ -1,19 +1,25 @@
<template>
<span class="mk-acct">
<span class="name">@{{ user.username }}</span>
- <span class="host" v-if="user.host">@{{ user.host }}</span>
+ <span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="user.host || detail || $store.state.settings.showFullAcct">@{{ user.host || host }}</span>
</span>
</template>
<script lang="ts">
import Vue from 'vue';
+import { host } from '../../../config';
export default Vue.extend({
- props: ['user']
+ props: ['user', 'detail'],
+ data() {
+ return {
+ host
+ };
+ }
});
</script>
<style lang="stylus" scoped>
.mk-acct
- > .host
+ > .host.fade
opacity 0.5
</style>
diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index b274eaa0a0..ea05afd6dc 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -125,7 +125,7 @@ export default Vue.extend({
}
if (this.type == 'user') {
- const cacheKey = 'autocomplete:user:' + this.q;
+ const cacheKey = `autocomplete:user:${this.q}`;
const cache = sessionStorage.getItem(cacheKey);
if (cache) {
const users = JSON.parse(cache);
@@ -148,7 +148,7 @@ export default Vue.extend({
this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
this.fetching = false;
} else {
- const cacheKey = 'autocomplete:hashtag:' + this.q;
+ const cacheKey = `autocomplete:hashtag:${this.q}`;
const cache = sessionStorage.getItem(cacheKey);
if (cache) {
const hashtags = JSON.parse(cache);
diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue
index c5ac74e537..a2b0fc6bd3 100644
--- a/src/client/app/common/views/components/avatar.vue
+++ b/src/client/app/common/views/components/avatar.vue
@@ -1,15 +1,15 @@
<template>
- <span class="mk-avatar" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
- <span class="inner" :style="style"></span>
+ <span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
+ <span class="inner" :style="icon"></span>
</span>
- <span class="mk-avatar" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
- <span class="inner" :style="style"></span>
+ <span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
+ <span class="inner" :style="icon"></span>
</span>
- <router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
- <span class="inner" :style="style"></span>
+ <router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
+ <span class="inner" :style="icon"></span>
</router-link>
- <router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
- <span class="inner" :style="style"></span>
+ <router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
+ <span class="inner" :style="icon"></span>
</router-link>
</template>
@@ -43,6 +43,11 @@ export default Vue.extend({
},
style(): any {
return {
+ borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
+ };
+ },
+ icon(): any {
+ return {
backgroundColor: this.lightmode
? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`
: this.user.avatarColor && this.user.avatarColor.length == 3
diff --git a/src/client/app/common/views/components/connect-failed.troubleshooter.vue b/src/client/app/common/views/components/connect-failed.troubleshooter.vue
index 6c23cc7969..f64cae6b4b 100644
--- a/src/client/app/common/views/components/connect-failed.troubleshooter.vue
+++ b/src/client/app/common/views/components/connect-failed.troubleshooter.vue
@@ -57,7 +57,7 @@ export default Vue.extend({
}
// Check internet connection
- fetch('https://google.com?rand=' + Math.random(), {
+ fetch(`https://google.com?rand=${Math.random()}`, {
mode: 'no-cors'
}).then(() => {
this.internet = true;
diff --git a/src/client/app/common/views/components/cw-button.vue b/src/client/app/common/views/components/cw-button.vue
new file mode 100644
index 0000000000..06087edc93
--- /dev/null
+++ b/src/client/app/common/views/components/cw-button.vue
@@ -0,0 +1,44 @@
+<template>
+<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh" @click="toggle">{{ value ? '%i18n:@hide%' : '%i18n:@show%' }}</button>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ value: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ methods: {
+ toggle() {
+ this.$emit('input', !this.value);
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+ 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
+
+ &:hover
+ background isDark ? #707b97 : #bbc4ce
+
+.nrvgflfuaxwgkxoynpnumyookecqrrvh[data-darkmode]
+ root(true)
+
+.nrvgflfuaxwgkxoynpnumyookecqrrvh:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue
index b432a2308d..fea19d917e 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.game.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.game.vue
@@ -50,15 +50,15 @@
</div>
<div class="player" v-if="game.isEnded">
- <el-button-group>
- <el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</el-button>
- <el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</el-button>
- </el-button-group>
+ <div>
+ <button @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</button>
+ <button @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</button>
+ </div>
<span>{{ logPos }} / {{ logs.length }}</span>
- <el-button-group>
- <el-button type="primary" @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</el-button>
- <el-button type="primary" @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</el-button>
- </el-button-group>
+ <div>
+ <button @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</button>
+ <button @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</button>
+ </div>
</div>
<div class="info">
@@ -159,11 +159,9 @@ export default Vue.extend({
canPutEverywhere: this.game.settings.canPutEverywhere,
loopedBoard: this.game.settings.loopedBoard
});
- this.logs.forEach((log, i) => {
- if (i < v) {
- this.o.put(log.color, log.pos);
- }
- });
+ for (const log of this.logs.slice(0, v)) {
+ this.o.put(log.color, log.pos);
+ }
this.$forceUpdate();
}
},
diff --git a/src/client/app/common/views/components/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue
index fa88aeaaf4..d23902aae7 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.index.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.index.vue
@@ -3,7 +3,6 @@
<h1>%i18n:@title%</h1>
<p>%i18n:@sub-title%</p>
<div class="play">
- <!--<el-button round>フリーマッチ(準備中)</el-button>-->
<form-button primary round @click="match">%i18n:@invite%</form-button>
<details>
<summary>%i18n:@rule%</summary>
diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue
index aed8718dd0..fef833d63e 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.room.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.room.vue
@@ -59,11 +59,6 @@
</header>
<div>
- <el-alert v-for="message in messages"
- :title="message.text"
- :type="message.type"
- :key="message.id"/>
-
<template v-for="item in form">
<mk-switch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm(item)">{{ item.desc || '' }}</mk-switch>
@@ -93,7 +88,7 @@
</header>
<div>
- <el-input v-model="item.value" @change="onChangeForm(item)"/>
+ <input v-model="item.value" @change="onChangeForm(item)"/>
</div>
</div>
</template>
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 422a3da050..6f8152cea2 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -1,5 +1,8 @@
import Vue from 'vue';
+import cwButton from './cw-button.vue';
+import tagCloud from './tag-cloud.vue';
+import trends from './trends.vue';
import analogClock from './analog-clock.vue';
import menu from './menu.vue';
import noteHeader from './note-header.vue';
@@ -40,6 +43,9 @@ import uiSelect from './ui/select.vue';
import formButton from './ui/form/button.vue';
import formRadio from './ui/form/radio.vue';
+Vue.component('mk-cw-button', cwButton);
+Vue.component('mk-tag-cloud', tagCloud);
+Vue.component('mk-trends', trends);
Vue.component('mk-analog-clock', analogClock);
Vue.component('mk-menu', menu);
Vue.component('mk-note-header', noteHeader);
diff --git a/src/client/app/common/views/components/media-banner.vue b/src/client/app/common/views/components/media-banner.vue
new file mode 100644
index 0000000000..211dbf0208
--- /dev/null
+++ b/src/client/app/common/views/components/media-banner.vue
@@ -0,0 +1,90 @@
+<template>
+<div class="mk-media-banner">
+ <div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false">
+ <span class="icon">%fa:exclamation-triangle%</span>
+ <b>%i18n:@sensitive%</b>
+ <span>%i18n:@click-to-show%</span>
+ </div>
+ <div class="audio" v-else-if="media.type.startsWith('audio')">
+ <audio class="audio"
+ :src="media.url"
+ :title="media.name"
+ controls
+ ref="audio"
+ preload="metadata" />
+ </div>
+ <a class="download" v-else
+ :href="media.url"
+ :title="media.name"
+ :download="media.name"
+ >
+ <span class="icon">%fa:download%</span>
+ <b>{{ media.name }}</b>
+ </a>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ media: {
+ type: Object,
+ required: true
+ }
+ },
+ data() {
+ return {
+ hide: true
+ };
+ }
+})
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+ width 100%
+ border-radius 4px
+ margin-top 4px
+ overflow hidden
+
+ > .download,
+ > .sensitive
+ display flex
+ align-items center
+ font-size 12px
+ padding 8px 12px
+ white-space nowrap
+
+ > *
+ display block
+
+ > b
+ overflow hidden
+ text-overflow ellipsis
+
+ > *:not(:last-child)
+ margin-right .2em
+
+ > .icon
+ font-size 1.6em
+
+ > .download
+ background isDark ? #21242d : #f7f7f7
+
+ > .sensitive
+ background #111
+ color #fff
+
+ > .audio
+ .audio
+ display block
+ width 100%
+
+.mk-media-banner[data-darkmode]
+ root(true)
+
+.mk-media-banner:not([data-darkmode])
+ root(false)
+</style>
diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue
index cdfc2c8d3c..d83d6f85cd 100644
--- a/src/client/app/common/views/components/media-list.vue
+++ b/src/client/app/common/views/components/media-list.vue
@@ -1,18 +1,27 @@
<template>
<div class="mk-media-list">
- <div :data-count="mediaList.length" ref="grid">
- <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 :raw="raw"/>
- </template>
+ <template v-for="media in mediaList.filter(media => !previewable(media))">
+ <x-banner :media="media" :key="media.id"/>
+ </template>
+ <div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
+ <div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid">
+ <template v-for="media in mediaList">
+ <mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
+ <mk-media-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
+ </template>
+ </div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
+import XBanner from './media-banner.vue';
export default Vue.extend({
+ components: {
+ XBanner
+ },
props: {
mediaList: {
required: true
@@ -22,70 +31,80 @@ export default Vue.extend({
}
},
mounted() {
- // for Safari bug
- this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` : '128px';
+ //#region for Safari bug
+ if (this.$refs.grid) {
+ this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` : '128px';
+ }
+ //#endregion
+ },
+ methods: {
+ previewable(file) {
+ return file.type.startsWith('video') || file.type.startsWith('image');
+ }
}
});
</script>
<style lang="stylus" scoped>
.mk-media-list
- width 100%
+ > .gird-container
+ width 100%
+ margin-top 4px
- &:before
- content ''
- display block
- padding-top 56.25% // 16:9
+ &:before
+ content ''
+ display block
+ padding-top 56.25% // 16:9
- > div
- position absolute
- top 0
- right 0
- bottom 0
- left 0
- display grid
- grid-gap 4px
+ > div
+ position absolute
+ top 0
+ right 0
+ bottom 0
+ left 0
+ display grid
+ grid-gap 4px
- > *
- overflow hidden
- border-radius 4px
+ > *
+ overflow hidden
+ border-radius 4px
- &[data-count="1"]
- grid-template-rows 1fr
+ &[data-count="1"]
+ grid-template-rows 1fr
- &[data-count="2"]
- grid-template-columns 1fr 1fr
- grid-template-rows 1fr
+ &[data-count="2"]
+ grid-template-columns 1fr 1fr
+ grid-template-rows 1fr
- &[data-count="3"]
- grid-template-columns 1fr 0.5fr
- grid-template-rows 1fr 1fr
+ &[data-count="3"]
+ grid-template-columns 1fr 0.5fr
+ grid-template-rows 1fr 1fr
- > *:nth-child(1)
- grid-row 1 / 3
+ > *:nth-child(1)
+ grid-row 1 / 3
- > *:nth-child(3)
- grid-column 2 / 3
- grid-row 2 / 3
+ > *:nth-child(3)
+ grid-column 2 / 3
+ grid-row 2 / 3
- &[data-count="4"]
- grid-template-columns 1fr 1fr
- grid-template-rows 1fr 1fr
+ &[data-count="4"]
+ grid-template-columns 1fr 1fr
+ grid-template-rows 1fr 1fr
- > *:nth-child(1)
- grid-column 1 / 2
- grid-row 1 / 2
+ > *:nth-child(1)
+ grid-column 1 / 2
+ grid-row 1 / 2
- > *:nth-child(2)
- grid-column 2 / 3
- grid-row 1 / 2
+ > *:nth-child(2)
+ grid-column 2 / 3
+ grid-row 1 / 2
- > *:nth-child(3)
- grid-column 1 / 2
- grid-row 2 / 3
+ > *:nth-child(3)
+ grid-column 1 / 2
+ grid-row 2 / 3
- > *:nth-child(4)
- grid-column 2 / 3
- grid-row 2 / 3
+ > *:nth-child(4)
+ grid-column 2 / 3
+ grid-row 2 / 3
</style>
diff --git a/src/client/app/common/views/components/menu.vue b/src/client/app/common/views/components/menu.vue
index 9b16732b9a..fba7e235e0 100644
--- a/src/client/app/common/views/components/menu.vue
+++ b/src/client/app/common/views/components/menu.vue
@@ -1,5 +1,5 @@
<template>
-<div class="mk-menu">
+<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv">
<div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ hukidasi }" ref="popover">
<template v-for="item in items">
@@ -108,7 +108,7 @@ export default Vue.extend({
easing: 'easeInBack',
complete: () => {
this.$emit('closed');
- this.$destroy();
+ this.destroyDom();
}
});
}
@@ -119,9 +119,10 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-$border-color = rgba(27, 31, 35, 0.15)
+root(isDark)
+ $bg-color = isDark ? #2c303c : #fff
+ $border-color = rgba(27, 31, 35, 0.15)
-.mk-menu
position initial
> .backdrop
@@ -131,14 +132,14 @@ $border-color = rgba(27, 31, 35, 0.15)
z-index 10000
width 100%
height 100%
- background rgba(#000, 0.1)
+ background rgba(#000, isDark ? 0.5 : 0.1)
opacity 0
> .popover
position absolute
z-index 10001
padding 8px 0
- background #fff
+ background $bg-color
border 1px solid $border-color
border-radius 4px
box-shadow 0 3px 12px rgba(27, 31, 35, 0.15)
@@ -172,12 +173,13 @@ $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 $bg-color
> button
display block
padding 8px 16px
width 100%
+ color isDark ? #d6dce2 : #111
&:hover
color $theme-color-foreground
@@ -191,6 +193,12 @@ $border-color = rgba(27, 31, 35, 0.15)
> div
margin 8px 0
height 1px
- background #eee
+ background isDark ? #1c2023 : #eee
+
+.onchrpzrvnoruiaenfcqvccjfuupzzwv[data-darkmode]
+ root(true)
+
+.onchrpzrvnoruiaenfcqvccjfuupzzwv:not([data-darkmode])
+ root(false)
</style>
diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue
index 30143b4f1d..1de41855df 100644
--- a/src/client/app/common/views/components/messaging-room.vue
+++ b/src/client/app/common/views/components/messaging-room.vue
@@ -3,7 +3,7 @@
@dragover.prevent.stop="onDragover"
@drop.prevent.stop="onDrop"
>
- <div class="stream">
+ <div class="body">
<p class="init" v-if="init">%fa:spinner .spin%%i18n:common.loading%</p>
<p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:@empty%</p>
<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages">%fa:flag%%i18n:@no-history%</p>
@@ -77,6 +77,12 @@ export default Vue.extend({
this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead);
+ if (this.isNaked) {
+ window.addEventListener('scroll', this.onScroll, { passive: true });
+ } else {
+ this.$el.addEventListener('scroll', this.onScroll, { passive: true });
+ }
+
document.addEventListener('visibilitychange', this.onVisibilitychange);
this.fetchMessages().then(() => {
@@ -90,6 +96,12 @@ export default Vue.extend({
this.connection.off('read', this.onRead);
this.connection.close();
+ if (this.isNaked) {
+ window.removeEventListener('scroll', this.onScroll);
+ } else {
+ this.$el.removeEventListener('scroll', this.onScroll);
+ }
+
document.removeEventListener('visibilitychange', this.onVisibilitychange);
},
@@ -226,6 +238,14 @@ export default Vue.extend({
}, 4000);
},
+ onScroll() {
+ const el = this.isNaked ? window.document.documentElement : this.$el;
+ const current = el.scrollTop + el.clientHeight;
+ if (current > el.scrollHeight - 1) {
+ this.showIndicator = false;
+ }
+ },
+
onVisibilitychange() {
if (document.hidden) return;
this.messages.forEach(message => {
@@ -251,7 +271,7 @@ root(isDark)
height 100%
background isDark ? #191b22 : #fff
- > .stream
+ > .body
width 100%
max-width 600px
margin 0 auto
diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.ts b/src/client/app/common/views/components/misskey-flavored-markdown.ts
index e97da4302c..224bd6f5de 100644
--- a/src/client/app/common/views/components/misskey-flavored-markdown.ts
+++ b/src/client/app/common/views/components/misskey-flavored-markdown.ts
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { VNode } from 'vue';
import * as emojilib from 'emojilib';
import { length } from 'stringz';
import parse from '../../../../../mfm/parse';
@@ -6,10 +6,7 @@ import getAcct from '../../../../../misc/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), []
-);
+import { concat } from '../../../../../prelude/array';
export default Vue.component('misskey-flavored-markdown', {
props: {
@@ -32,20 +29,20 @@ export default Vue.component('misskey-flavored-markdown', {
},
render(createElement) {
- let ast;
+ let ast: any[];
if (this.ast == null) {
// Parse text to ast
ast = parse(this.text);
} else {
- ast = this.ast;
+ ast = this.ast as any[];
}
let bigCount = 0;
let motionCount = 0;
// Parse ast to DOM
- const els = flatten(ast.map(token => {
+ const els = concat(ast.map((token): VNode[] => {
switch (token.type) {
case 'text': {
const text = token.content.replace(/(\r\n|\n|\r)/g, '\n');
@@ -56,12 +53,12 @@ export default Vue.component('misskey-flavored-markdown', {
x[x.length - 1].pop();
return x;
} else {
- return createElement('span', text.replace(/\n/g, ' '));
+ return [createElement('span', text.replace(/\n/g, ' '))];
}
}
case 'bold': {
- return createElement('b', token.bold);
+ return [createElement('b', token.bold)];
}
case 'big': {
@@ -95,23 +92,23 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'url': {
- return createElement(MkUrl, {
+ return [createElement(MkUrl, {
props: {
url: token.content,
target: '_blank'
}
- });
+ })];
}
case 'link': {
- return createElement('a', {
+ return [createElement('a', {
attrs: {
class: 'link',
href: token.url,
target: '_blank',
title: token.url
}
- }, token.title);
+ }, token.title)];
}
case 'mention': {
@@ -129,16 +126,16 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'hashtag': {
- return createElement('a', {
+ return [createElement('a', {
attrs: {
href: `${url}/tags/${encodeURIComponent(token.hashtag)}`,
target: '_blank'
}
- }, token.content);
+ }, token.content)];
}
case 'code': {
- return createElement('pre', {
+ return [createElement('pre', {
class: 'code'
}, [
createElement('code', {
@@ -146,15 +143,15 @@ export default Vue.component('misskey-flavored-markdown', {
innerHTML: token.html
}
})
- ]);
+ ])];
}
case 'inline-code': {
- return createElement('code', {
+ return [createElement('code', {
domProps: {
innerHTML: token.html
}
- });
+ })];
}
case 'quote': {
@@ -164,58 +161,51 @@ export default Vue.component('misskey-flavored-markdown', {
const x = text2.split('\n')
.map(t => [createElement('span', t), createElement('br')]);
x[x.length - 1].pop();
- return createElement('div', {
+ return [createElement('div', {
attrs: {
class: 'quote'
}
- }, x);
+ }, x)];
} else {
- return createElement('span', {
+ return [createElement('span', {
attrs: {
class: 'quote'
}
- }, text2.replace(/\n/g, ' '));
+ }, text2.replace(/\n/g, ' '))];
}
}
case 'title': {
- return createElement('div', {
+ return [createElement('div', {
attrs: {
class: 'title'
}
- }, token.title);
+ }, token.title)];
}
case 'emoji': {
const emoji = emojilib.lib[token.emoji];
- return createElement('span', emoji ? emoji.char : token.content);
+ return [createElement('span', emoji ? emoji.char : token.content)];
}
case 'search': {
- return createElement(MkGoogle, {
+ return [createElement(MkGoogle, {
props: {
q: token.query
}
- });
+ })];
}
default: {
console.log('unknown ast type:', token.type);
- }
- }
- }));
- const _els = [];
- els.forEach((el, i) => {
- if (el.tag == 'br') {
- if (!['div', 'pre'].includes(els[i - 1].tag)) {
- _els.push(el);
+ return [];
}
- } else {
- _els.push(el);
}
- });
+ }));
+ // el.tag === 'br' のとき i !== 0 が保証されるため、短絡評価により els[i - 1] は配列外参照しない
+ const _els = els.filter((el, i) => !(el.tag === 'br' && ['div', 'pre'].includes(els[i - 1].tag)));
return createElement('span', _els);
}
});
diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/app/common/views/components/note-menu.vue
index 27a49a6536..c9912fb1e2 100644
--- a/src/client/app/common/views/components/note-menu.vue
+++ b/src/client/app/common/views/components/note-menu.vue
@@ -6,17 +6,27 @@
<script lang="ts">
import Vue from 'vue';
+import { url } from '../../../config';
+import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
export default Vue.extend({
props: ['note', 'source', 'compact'],
computed: {
items() {
- const items = [];
- items.push({
+ const items = [{
+ icon: '%fa:info-circle%',
+ text: '%i18n:@detail%',
+ action: this.detail
+ }, {
+ icon: '%fa:link%',
+ text: '%i18n:@copy-link%',
+ action: this.copyLink
+ }, null, {
icon: '%fa:star%',
text: '%i18n:@favorite%',
action: this.favorite
- });
+ }];
+
if (this.note.userId == this.$store.state.i.id) {
items.push({
icon: '%fa:thumbtack%',
@@ -42,11 +52,19 @@ export default Vue.extend({
}
},
methods: {
+ detail() {
+ this.$router.push(`/notes/${ this.note.id }`);
+ },
+
+ copyLink() {
+ copyToClipboard(`${url}/notes/${ this.note.id }`);
+ },
+
pin() {
(this as any).api('i/pin', {
noteId: this.note.id
}).then(() => {
- this.$destroy();
+ this.destroyDom();
});
},
@@ -55,7 +73,7 @@ export default Vue.extend({
(this as any).api('notes/delete', {
noteId: this.note.id
}).then(() => {
- this.$destroy();
+ this.destroyDom();
});
},
@@ -63,13 +81,13 @@ export default Vue.extend({
(this as any).api('notes/favorites/create', {
noteId: this.note.id
}).then(() => {
- this.$destroy();
+ this.destroyDom();
});
},
closed() {
this.$nextTick(() => {
- this.$destroy();
+ this.destroyDom();
});
}
}
diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue
index 115c934c8b..30d9799fec 100644
--- a/src/client/app/common/views/components/poll-editor.vue
+++ b/src/client/app/common/views/components/poll-editor.vue
@@ -20,6 +20,7 @@
<script lang="ts">
import Vue from 'vue';
+import { erase } from '../../../../../prelude/array';
export default Vue.extend({
data() {
return {
@@ -53,7 +54,7 @@ export default Vue.extend({
get() {
return {
- choices: this.choices.filter(choice => choice != '')
+ choices: erase('', this.choices)
}
},
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
index 660247edbc..4fe51d219b 100644
--- a/src/client/app/common/views/components/poll.vue
+++ b/src/client/app/common/views/components/poll.vue
@@ -21,6 +21,7 @@
<script lang="ts">
import Vue from 'vue';
+import { sum } from '../../../../../prelude/array';
export default Vue.extend({
props: ['note'],
data() {
@@ -33,7 +34,7 @@ export default Vue.extend({
return this.note.poll;
},
total(): number {
- return this.poll.choices.reduce((a, b) => a + b.votes, 0);
+ return sum(this.poll.choices.map(x => x.votes));
},
isVoted(): boolean {
return this.poll.choices.some(c => c.isVoted);
diff --git a/src/client/app/common/views/components/reaction-icon.vue b/src/client/app/common/views/components/reaction-icon.vue
index 46886b8ab2..c668efac6b 100644
--- a/src/client/app/common/views/components/reaction-icon.vue
+++ b/src/client/app/common/views/components/reaction-icon.vue
@@ -1,17 +1,17 @@
<template>
<span class="mk-reaction-icon">
- <img v-if="reaction == 'like'" src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%">
- <img v-if="reaction == 'love'" src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%">
- <img v-if="reaction == 'laugh'" src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%">
- <img v-if="reaction == 'hmm'" src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%">
- <img v-if="reaction == 'surprise'" src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%">
- <img v-if="reaction == 'congrats'" src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%">
- <img v-if="reaction == 'angry'" src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%">
- <img v-if="reaction == 'confused'" src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%">
- <img v-if="reaction == 'rip'" src="/assets/reactions/rip.png" alt="%i18n:common.reactions.rip%">
+ <img v-if="reaction == 'like'" src="https://twemoji.maxcdn.com/2/svg/1f44d.svg" alt="%i18n:common.reactions.like%">
+ <img v-if="reaction == 'love'" src="https://twemoji.maxcdn.com/2/svg/2764.svg" alt="%i18n:common.reactions.love%">
+ <img v-if="reaction == 'laugh'" src="https://twemoji.maxcdn.com/2/svg/1f606.svg" alt="%i18n:common.reactions.laugh%">
+ <img v-if="reaction == 'hmm'" src="https://twemoji.maxcdn.com/2/svg/1f914.svg" alt="%i18n:common.reactions.hmm%">
+ <img v-if="reaction == 'surprise'" src="https://twemoji.maxcdn.com/2/svg/1f62e.svg" alt="%i18n:common.reactions.surprise%">
+ <img v-if="reaction == 'congrats'" src="https://twemoji.maxcdn.com/2/svg/1f389.svg" alt="%i18n:common.reactions.congrats%">
+ <img v-if="reaction == 'angry'" src="https://twemoji.maxcdn.com/2/svg/1f4a2.svg" alt="%i18n:common.reactions.angry%">
+ <img v-if="reaction == 'confused'" src="https://twemoji.maxcdn.com/2/svg/1f625.svg" alt="%i18n:common.reactions.confused%">
+ <img v-if="reaction == 'rip'" src="https://twemoji.maxcdn.com/2/svg/1f607.svg" alt="%i18n:common.reactions.rip%">
<template v-if="reaction == 'pudding'">
- <img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="/assets/reactions/sushi.png" alt="%i18n:common.reactions.pudding%">
- <img v-else src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%">
+ <img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="https://twemoji.maxcdn.com/2/svg/1f363.svg" alt="%i18n:common.reactions.pudding%">
+ <img v-else src="https://twemoji.maxcdn.com/2/svg/1f36e.svg" alt="%i18n:common.reactions.pudding%">
</template>
</span>
</template>
diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue
index a455afbf7d..a4828c987b 100644
--- a/src/client/app/common/views/components/reaction-picker.vue
+++ b/src/client/app/common/views/components/reaction-picker.vue
@@ -95,7 +95,7 @@ export default Vue.extend({
reaction: reaction
}).then(() => {
if (this.cb) this.cb();
- this.$destroy();
+ this.destroyDom();
});
},
onMouseover(e) {
@@ -120,7 +120,7 @@ export default Vue.extend({
scale: 0.5,
duration: 200,
easing: 'easeInBack',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}
}
diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue
index 5230ac371a..b1c6782e93 100644
--- a/src/client/app/common/views/components/signin.vue
+++ b/src/client/app/common/views/components/signin.vue
@@ -78,7 +78,7 @@ export default Vue.extend({
cursor wait !important
> .avatar
- margin 16px auto 0 auto
+ margin 0 auto 0 auto
width 64px
height 64px
background #ddd
diff --git a/src/client/app/common/views/components/tag-cloud.vue b/src/client/app/common/views/components/tag-cloud.vue
new file mode 100644
index 0000000000..5f2cc5276a
--- /dev/null
+++ b/src/client/app/common/views/components/tag-cloud.vue
@@ -0,0 +1,90 @@
+<template>
+<div class="jtivnzhfwquxpsfidertopbmwmchmnmo">
+ <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+ <p class="empty" v-else-if="tags.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
+ <div v-else>
+ <vue-word-cloud
+ :words="tags.slice(0, 20).map(x => [x.name, x.count])"
+ :color="color"
+ :spacing="1">
+ <template slot-scope="{word, text, weight}">
+ <div style="cursor: pointer;" :title="weight">
+ {{ text }}
+ </div>
+ </template>
+ </vue-word-cloud>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as VueWordCloud from 'vuewordcloud';
+
+export default Vue.extend({
+ components: {
+ [VueWordCloud.name]: VueWordCloud
+ },
+ data() {
+ return {
+ tags: [],
+ fetching: true,
+ clock: null
+ };
+ },
+ mounted() {
+ this.fetch();
+ this.clock = setInterval(this.fetch, 1000 * 60);
+ },
+ beforeDestroy() {
+ clearInterval(this.clock);
+ },
+ methods: {
+ fetch() {
+ (this as any).api('aggregation/hashtags').then(tags => {
+ this.tags = tags;
+ this.fetching = false;
+ });
+ },
+ color([, weight]) {
+ const peak = Math.max.apply(null, this.tags.map(x => x.count));
+ const w = weight / peak;
+
+ if (w > 0.9) {
+ return this.$store.state.device.darkmode ? '#ff4e69' : '#ff4e69';
+ } else if (w > 0.5) {
+ return this.$store.state.device.darkmode ? '#3bc4c7' : '#3bc4c7';
+ } else {
+ return this.$store.state.device.darkmode ? '#fff' : '#555';
+ }
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+ height 100%
+ width 100%
+
+ > .fetching
+ > .empty
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > [data-fa]
+ margin-right 4px
+
+ > div
+ height 100%
+ width 100%
+
+.jtivnzhfwquxpsfidertopbmwmchmnmo[data-darkmode]
+ root(true)
+
+.jtivnzhfwquxpsfidertopbmwmchmnmo:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/common/views/widgets/hashtags.chart.vue b/src/client/app/common/views/components/trends.chart.vue
index 723a3947f8..723a3947f8 100644
--- a/src/client/app/common/views/widgets/hashtags.chart.vue
+++ b/src/client/app/common/views/components/trends.chart.vue
diff --git a/src/client/app/common/views/components/trends.vue b/src/client/app/common/views/components/trends.vue
new file mode 100644
index 0000000000..0042dbe853
--- /dev/null
+++ b/src/client/app/common/views/components/trends.vue
@@ -0,0 +1,103 @@
+<template>
+<div class="csqvmxybqbycalfhkxvyfrgbrdalkaoc">
+ <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+ <p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
+ <!-- トランジションを有効にするとなぜかメモリリークする -->
+ <transition-group v-else tag="div" name="chart">
+ <div v-for="stat in stats" :key="stat.tag">
+ <div class="tag">
+ <router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
+ <p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p>
+ </div>
+ <x-chart class="chart" :src="stat.chart"/>
+ </div>
+ </transition-group>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XChart from './trends.chart.vue';
+
+export default Vue.extend({
+ components: {
+ XChart
+ },
+ data() {
+ return {
+ stats: [],
+ fetching: true,
+ clock: null
+ };
+ },
+ mounted() {
+ this.fetch();
+ this.clock = setInterval(this.fetch, 1000 * 60);
+ },
+ beforeDestroy() {
+ clearInterval(this.clock);
+ },
+ methods: {
+ fetch() {
+ (this as any).api('hashtags/trend').then(stats => {
+ this.stats = stats;
+ this.fetching = false;
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+ > .fetching
+ > .empty
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > [data-fa]
+ margin-right 4px
+
+ > div
+ .chart-move
+ transition transform 1s ease
+
+ > div
+ display flex
+ align-items center
+ padding 14px 16px
+
+ &:not(:last-child)
+ border-bottom solid 1px isDark ? #393f4f : #eee
+
+ > .tag
+ flex 1
+ overflow hidden
+ font-size 14px
+ color isDark ? #9baec8 : #65727b
+
+ > a
+ display block
+ width 100%
+ white-space nowrap
+ overflow hidden
+ text-overflow ellipsis
+ color inherit
+
+ > p
+ margin 0
+ font-size 75%
+ opacity 0.7
+
+ > .chart
+ height 30px
+
+.csqvmxybqbycalfhkxvyfrgbrdalkaoc[data-darkmode]
+ root(true)
+
+.csqvmxybqbycalfhkxvyfrgbrdalkaoc:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/common/views/components/ui/card.vue b/src/client/app/common/views/components/ui/card.vue
index 05c51bca6b..aa16b557e1 100644
--- a/src/client/app/common/views/components/ui/card.vue
+++ b/src/client/app/common/views/components/ui/card.vue
@@ -24,19 +24,34 @@ export default Vue.extend({
root(isDark)
margin 16px
- padding 16px
color isDark ? #fff : #000
background isDark ? #282C37 : #fff
box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12)
- @media (min-width 500px)
- padding 32px
-
> header
- font-weight normal
- font-size 24px
+ padding 16px
+ font-weight bold
+ font-size 20px
color isDark ? #fff : #444
+ @media (min-width 500px)
+ padding 24px 32px
+
+ > section
+ padding 20px 16px
+ border-top solid 1px isDark ? rgba(#000, 0.3) : rgba(#000, 0.1)
+
+ @media (min-width 500px)
+ padding 32px
+
+ &.fit-top
+ padding-top 0
+
+ > header
+ margin-bottom 16px
+ font-weight bold
+ color isDark ? #fff : #444
+
.ui-card[data-darkmode]
root(true)
diff --git a/src/client/app/common/views/components/ui/radio.vue b/src/client/app/common/views/components/ui/radio.vue
index 04a46c5a96..dcdda1cf0e 100644
--- a/src/client/app/common/views/components/ui/radio.vue
+++ b/src/client/app/common/views/components/ui/radio.vue
@@ -55,7 +55,7 @@ export default Vue.extend({
root(isDark)
display inline-block
- margin 32px 32px 32px 0
+ margin 0 32px 0 0
cursor pointer
transition all 0.3s
diff --git a/src/client/app/common/views/components/ui/switch.vue b/src/client/app/common/views/components/ui/switch.vue
index a9e00d73d2..e88b867801 100644
--- a/src/client/app/common/views/components/ui/switch.vue
+++ b/src/client/app/common/views/components/ui/switch.vue
@@ -64,6 +64,12 @@ root(isDark)
cursor pointer
transition all 0.3s
+ &:first-child
+ margin-top 0
+
+ &:last-child
+ margin-bottom 0
+
> *
user-select none
@@ -89,6 +95,7 @@ root(isDark)
> .button
display inline-block
+ flex-shrink 0
margin 3px 0 0 0
width 34px
height 14px
diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue
index 242d9ba5c6..f9b8415b5b 100644
--- a/src/client/app/common/views/components/url-preview.vue
+++ b/src/client/app/common/views/components/url-preview.vue
@@ -8,13 +8,13 @@
</blockquote>
</div>
<div v-else class="mk-url-preview">
- <a :href="url" target="_blank" :title="url" v-if="!fetching">
+ <a :class="{ mini }" :href="url" target="_blank" :title="url" v-if="!fetching">
<div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div>
<article>
<header>
<h1>{{ title }}</h1>
</header>
- <p>{{ description }}</p>
+ <p>{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
<footer>
<img class="icon" v-if="icon" :src="icon"/>
<p>{{ sitename }}</p>
@@ -118,6 +118,12 @@ export default Vue.extend({
type: Boolean,
required: false,
default: false
+ },
+
+ mini: {
+ type: Boolean,
+ required: false,
+ default: false
}
},
@@ -164,7 +170,7 @@ export default Vue.extend({
return;
}
- fetch('/url?url=' + encodeURIComponent(this.url)).then(res => {
+ fetch(`/url?url=${encodeURIComponent(this.url)}`).then(res => {
res.json().then(info => {
if (info.url == null) return;
this.title = info.title;
@@ -293,6 +299,29 @@ root(isDark)
width 12px
height 12px
+ &.mini
+ font-size 10px
+
+ > .thumbnail
+ position relative
+ width 100%
+ height 60px
+
+ > article
+ left 0
+ width 100%
+ padding 8px
+
+ > header
+ margin-bottom 4px
+
+ > footer
+ margin-top 4px
+
+ > img
+ width 12px
+ height 12px
+
.mk-url-preview[data-darkmode]
root(true)
diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue
index e6ffe4466d..04a1f30135 100644
--- a/src/client/app/common/views/components/url.vue
+++ b/src/client/app/common/views/components/url.vue
@@ -12,6 +12,7 @@
<script lang="ts">
import Vue from 'vue';
+import { toUnicode as decodePunycode } from 'punycode';
export default Vue.extend({
props: ['url', 'target'],
data() {
@@ -27,11 +28,11 @@ export default Vue.extend({
created() {
const url = new URL(this.url);
this.schema = url.protocol;
- this.hostname = url.hostname;
+ this.hostname = decodePunycode(url.hostname);
this.port = url.port;
- this.pathname = url.pathname;
- this.query = url.search;
- this.hash = url.hash;
+ this.pathname = decodeURIComponent(url.pathname);
+ this.query = decodeURIComponent(url.search);
+ this.hash = decodeURIComponent(url.hash);
}
});
</script>
diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
index 4691604e57..1830b1832e 100644
--- a/src/client/app/common/views/components/visibility-chooser.vue
+++ b/src/client/app/common/views/components/visibility-chooser.vue
@@ -47,7 +47,7 @@ export default Vue.extend({
props: ['source', 'compact'],
data() {
return {
- v: this.$store.state.device.visibility || 'public'
+ v: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility
}
},
mounted() {
@@ -97,9 +97,11 @@ export default Vue.extend({
},
methods: {
choose(visibility) {
- this.$store.commit('device/setVisibility', visibility);
+ if (this.$store.state.settings.rememberNoteVisibility) {
+ this.$store.commit('device/setVisibility', visibility);
+ }
this.$emit('chosen', visibility);
- this.$destroy();
+ this.destroyDom();
},
close() {
(this.$refs.backdrop as any).style.pointerEvents = 'none';
@@ -117,7 +119,7 @@ export default Vue.extend({
scale: 0.5,
duration: 200,
easing: 'easeInBack',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}
}
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 5a8b9df476..965ec78559 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -1,22 +1,24 @@
<template>
<div class="mk-welcome-timeline">
- <div v-for="note in notes">
- <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>
- <span class="username">@{{ note.user | acct }}</span>
- <div class="info">
- <router-link class="created-at" :to="note | notePage">
- <mk-time :time="note.createdAt"/>
- </router-link>
+ <transition-group name="ldzpakcixzickvggyixyrhqwjaefknon" tag="div">
+ <div v-for="note in notes" :key="note.id">
+ <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>
+ <span class="username">@{{ note.user | acct }}</span>
+ <div class="info">
+ <router-link class="created-at" :to="note | notePage">
+ <mk-time :time="note.createdAt"/>
+ </router-link>
+ </div>
+ </header>
+ <div class="text">
+ <misskey-flavored-markdown v-if="note.text" :text="note.text"/>
</div>
- </header>
- <div class="text">
- <misskey-flavored-markdown v-if="note.text" :text="note.text"/>
</div>
</div>
- </div>
+ </transition-group>
</div>
</template>
@@ -31,15 +33,30 @@ export default Vue.extend({
default: undefined
}
},
+
data() {
return {
fetching: true,
- notes: []
+ notes: [],
+ connection: null,
+ connectionId: null
};
},
+
mounted() {
this.fetch();
+
+ this.connection = (this as any).os.streams.localTimelineStream.getConnection();
+ this.connectionId = (this as any).os.streams.localTimelineStream.use();
+
+ this.connection.on('note', this.onNote);
+ },
+
+ beforeDestroy() {
+ this.connection.off('note', this.onNote);
+ (this as any).os.streams.localTimelineStream.dispose(this.connectionId);
},
+
methods: {
fetch(cb?) {
this.fetching = true;
@@ -48,77 +65,93 @@ export default Vue.extend({
local: true,
reply: false,
renote: false,
- media: false,
- poll: false,
- bot: false
+ file: false,
+ poll: false
}).then(notes => {
this.notes = notes;
this.fetching = false;
});
- }
+ },
+
+ onNote(note) {
+ if (note.replyId != null) return;
+ if (note.renoteId != null) return;
+ if (note.poll != null) return;
+
+ this.notes.unshift(note);
+ },
}
});
</script>
<style lang="stylus" scoped>
+.ldzpakcixzickvggyixyrhqwjaefknon-enter
+.ldzpakcixzickvggyixyrhqwjaefknon-leave-to
+ opacity 0
+ transform translateY(-30px)
+
root(isDark)
background isDark ? #282C37 : #fff
> div
- padding 16px
- overflow-wrap break-word
- font-size .9em
- color isDark ? #fff : #4C4C4C
- border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05)
+ > *
+ transition transform .3s ease, opacity .3s ease
+
+ > div
+ padding 16px
+ overflow-wrap break-word
+ font-size .9em
+ color isDark ? #fff : #4C4C4C
+ border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05)
- &:after
- content ""
- display block
- clear both
+ &:after
+ content ""
+ display block
+ clear both
- > .avatar
- display block
- float left
- position -webkit-sticky
- position sticky
- top 16px
- width 42px
- height 42px
- border-radius 6px
+ > .avatar
+ display block
+ float left
+ position -webkit-sticky
+ position sticky
+ top 16px
+ width 42px
+ height 42px
+ border-radius 6px
- > .body
- float right
- width calc(100% - 42px)
- padding-left 12px
+ > .body
+ float right
+ width calc(100% - 42px)
+ padding-left 12px
- > header
- display flex
- align-items center
- margin-bottom 4px
- white-space nowrap
+ > header
+ display flex
+ align-items center
+ margin-bottom 4px
+ white-space nowrap
- > .name
- display block
- margin 0 .5em 0 0
- padding 0
- overflow hidden
- font-weight bold
- text-overflow ellipsis
- color isDark ? #fff : #627079
+ > .name
+ display block
+ margin 0 .5em 0 0
+ padding 0
+ overflow hidden
+ font-weight bold
+ text-overflow ellipsis
+ color isDark ? #fff : #627079
- > .username
- margin 0 .5em 0 0
- color isDark ? #606984 : #ccc
+ > .username
+ margin 0 .5em 0 0
+ color isDark ? #606984 : #ccc
- > .info
- margin-left auto
- font-size 0.9em
+ > .info
+ margin-left auto
+ font-size 0.9em
- > .created-at
- color isDark ? #606984 : #c0c0c0
+ > .created-at
+ color isDark ? #606984 : #c0c0c0
- > .text
- text-align left
+ > .text
+ text-align left
.mk-welcome-timeline[data-darkmode]
root(true)
diff --git a/src/client/app/common/views/directives/autocomplete.ts b/src/client/app/common/views/directives/autocomplete.ts
index b252cf5c1f..f7f8e9bf16 100644
--- a/src/client/app/common/views/directives/autocomplete.ts
+++ b/src/client/app/common/views/directives/autocomplete.ts
@@ -167,7 +167,7 @@ class Autocomplete {
private close() {
if (this.suggestion == null) return;
- this.suggestion.$destroy();
+ this.suggestion.destroyDom();
this.suggestion = null;
this.textarea.focus();
@@ -191,7 +191,7 @@ class Autocomplete {
const acct = renderAcct(value);
// 挿入
- this.text = trimmedBefore + '@' + acct + ' ' + after;
+ this.text = `${trimmedBefore}@${acct} ${after}`;
// キャレットを戻す
this.vm.$nextTick(() => {
@@ -207,7 +207,7 @@ class Autocomplete {
const after = source.substr(caret);
// 挿入
- this.text = trimmedBefore + '#' + value + ' ' + after;
+ this.text = `${trimmedBefore}#${value} ${after}`;
// キャレットを戻す
this.vm.$nextTick(() => {
diff --git a/src/client/app/common/views/filters/note.ts b/src/client/app/common/views/filters/note.ts
index a611dc8685..3c9c8b7485 100644
--- a/src/client/app/common/views/filters/note.ts
+++ b/src/client/app/common/views/filters/note.ts
@@ -1,5 +1,5 @@
import Vue from 'vue';
Vue.filter('notePage', note => {
- return '/notes/' + note.id;
+ return `/notes/${note.id}`;
});
diff --git a/src/client/app/common/views/filters/user.ts b/src/client/app/common/views/filters/user.ts
index ca0910fc53..e5220229b7 100644
--- a/src/client/app/common/views/filters/user.ts
+++ b/src/client/app/common/views/filters/user.ts
@@ -11,5 +11,5 @@ Vue.filter('userName', user => {
});
Vue.filter('userPage', (user, path?) => {
- return '/@' + Vue.filter('acct')(user) + (path ? '/' + path : '');
+ return `/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`;
});
diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue
index 13d855d20a..80a870a257 100644
--- a/src/client/app/common/views/pages/follow.vue
+++ b/src/client/app/common/views/pages/follow.vue
@@ -1,6 +1,6 @@
<template>
<div class="syxhndwprovvuqhmyvveewmbqayniwkv" v-if="!fetching" :data-darkmode="$store.state.device.darkmode">
- <div class="signed-in-as" v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + myName + '</b>')"></div>
+ <div class="signed-in-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${myName}`)"></div>
<main>
<div class="banner" :style="bannerStyle"></div>
@@ -32,7 +32,6 @@
<script lang="ts">
import Vue from 'vue';
import parseAcct from '../../../../../misc/acct/parse';
-import getUserName from '../../../../../misc/get-user-name';
import Progress from '../../../common/scripts/loading';
export default Vue.extend({
@@ -83,7 +82,7 @@ export default Vue.extend({
userId: this.user.id
});
} else {
- if (this.user.isLocked && this.user.hasPendingFollowRequestFromYou) {
+ if (this.user.hasPendingFollowRequestFromYou) {
this.user = await (this as any).api('following/requests/cancel', {
userId: this.user.id
});
diff --git a/src/client/app/common/views/widgets/analog-clock.vue b/src/client/app/common/views/widgets/analog-clock.vue
index 0de30228b3..04223f0d21 100644
--- a/src/client/app/common/views/widgets/analog-clock.vue
+++ b/src/client/app/common/views/widgets/analog-clock.vue
@@ -1,8 +1,8 @@
<template>
<div class="mkw-analog-clock">
- <mk-widget-container :naked="!(props.design % 2)" :show-header="false">
+ <mk-widget-container :naked="props.style % 2 === 0" :show-header="false">
<div class="mkw-analog-clock--body">
- <mk-analog-clock :dark="$store.state.device.darkmode" :smooth="!(props.design && ~props.design)"/>
+ <mk-analog-clock :dark="$store.state.device.darkmode" :smooth="props.style < 2"/>
</div>
</mk-widget-container>
</div>
@@ -13,13 +13,12 @@ import define from '../../../common/define-widget';
export default define({
name: 'analog-clock',
props: () => ({
- design: -1
+ style: 0
})
}).extend({
methods: {
func() {
- if (++this.props.design > 2)
- this.props.design = -1;
+ this.props.style = (this.props.style + 1) % 4;
this.save();
}
}
diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue
index 69b2a54fe9..f2fa720f52 100644
--- a/src/client/app/common/views/widgets/broadcast.vue
+++ b/src/client/app/common/views/widgets/broadcast.vue
@@ -1,6 +1,6 @@
<template>
-<div class="mkw-broadcast"
- :data-found="broadcasts.length != 0"
+<div class="anltbovirfeutcigvwgmgxipejaeozxi"
+ :data-found="announcements && announcements.length != 0"
:data-melt="props.design == 1"
:data-mobile="platform == 'mobile'"
>
@@ -14,18 +14,17 @@
</svg>
</div>
<p class="fetching" v-if="fetching">%i18n:@fetching%<mk-ellipsis/></p>
- <h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:@no-broadcasts%' : broadcasts[i].title }}</h1>
+ <h1 v-if="!fetching">{{ announcements.length == 0 ? '%i18n:@no-broadcasts%' : announcements[i].title }}</h1>
<p v-if="!fetching">
- <span v-if="broadcasts.length != 0" v-html="broadcasts[i].text"></span>
- <template v-if="broadcasts.length == 0">%i18n:@have-a-nice-day%</template>
+ <span v-if="announcements.length != 0" v-html="announcements[i].text"></span>
+ <template v-if="announcements.length == 0">%i18n:@have-a-nice-day%</template>
</p>
- <a v-if="broadcasts.length > 1" @click="next">%i18n:@next% &gt;&gt;</a>
+ <a v-if="announcements.length > 1" @click="next">%i18n:@next% &gt;&gt;</a>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
-import { lang } from '../../../config';
export default define({
name: 'broadcast',
@@ -37,26 +36,18 @@ export default define({
return {
i: 0,
fetching: true,
- broadcasts: []
+ announcements: []
};
},
mounted() {
(this as any).os.getMeta().then(meta => {
- let broadcasts = [];
- if (meta.broadcasts) {
- meta.broadcasts.forEach(broadcast => {
- if (broadcast[lang]) {
- broadcasts.push(broadcast[lang]);
- }
- });
- }
- this.broadcasts = broadcasts;
+ this.announcements = meta.broadcasts;
this.fetching = false;
});
},
methods: {
next() {
- if (this.i == this.broadcasts.length - 1) {
+ if (this.i == this.announcements.length - 1) {
this.i = 0;
} else {
this.i++;
@@ -75,7 +66,7 @@ export default define({
</script>
<style lang="stylus" scoped>
-.mkw-broadcast
+root(isDark)
padding 10px
border solid 1px #4078c0
border-radius 6px
@@ -135,22 +126,18 @@ export default define({
margin 0
font-size 0.95em
font-weight normal
- color #4078c0
+ color isDark ? #539eff : #4078c0
> p
display block
z-index 1
margin 0
font-size 0.7em
- color #555
+ color isDark ? #fff : #555
&.fetching
text-align center
- a
- color #555
- text-decoration underline
-
> a
display block
font-size 0.7em
@@ -159,4 +146,10 @@ export default define({
> p
color #fff
+.anltbovirfeutcigvwgmgxipejaeozxi[data-darkmode]
+ root(true)
+
+.anltbovirfeutcigvwgmgxipejaeozxi:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/common/views/widgets/hashtags.vue b/src/client/app/common/views/widgets/hashtags.vue
index 56520400b6..0cb6b2df10 100644
--- a/src/client/app/common/views/widgets/hashtags.vue
+++ b/src/client/app/common/views/widgets/hashtags.vue
@@ -4,20 +4,7 @@
<template slot="header">%fa:hashtag%%i18n:@title%</template>
<div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'">
- <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
- <p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
- <!-- トランジションを有効にするとなぜかメモリリークする -->
- <!-- <transition-group v-else tag="div" name="chart"> -->
- <div>
- <div v-for="stat in stats" :key="stat.tag">
- <div class="tag">
- <router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
- <p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p>
- </div>
- <x-chart class="chart" :src="stat.chart"/>
- </div>
- </div>
- <!-- </transition-group> -->
+ <mk-trends/>
</div>
</mk-widget-container>
</div>
@@ -25,7 +12,6 @@
<script lang="ts">
import define from '../../../common/define-widget';
-import XChart from './hashtags.chart.vue';
export default define({
name: 'hashtags',
@@ -33,89 +19,11 @@ export default define({
compact: false
})
}).extend({
- components: {
- XChart
- },
- data() {
- return {
- stats: [],
- fetching: true,
- clock: null
- };
- },
- mounted() {
- this.fetch();
- this.clock = setInterval(this.fetch, 1000 * 60);
- },
- beforeDestroy() {
- clearInterval(this.clock);
- },
methods: {
func() {
this.props.compact = !this.props.compact;
this.save();
- },
- fetch() {
- (this as any).api('hashtags/trend').then(stats => {
- this.stats = stats;
- this.fetching = false;
- });
}
}
});
</script>
-
-<style lang="stylus" scoped>
-root(isDark)
- .mkw-hashtags--body
- > .fetching
- > .empty
- margin 0
- padding 16px
- text-align center
- color #aaa
-
- > [data-fa]
- margin-right 4px
-
- > div
- .chart-move
- transition transform 1s ease
-
- > div
- display flex
- align-items center
- padding 14px 16px
-
- &:not(:last-child)
- border-bottom solid 1px isDark ? #393f4f : #eee
-
- > .tag
- flex 1
- overflow hidden
- font-size 14px
- color isDark ? #9baec8 : #65727b
-
- > a
- display block
- width 100%
- white-space nowrap
- overflow hidden
- text-overflow ellipsis
- color inherit
-
- > p
- margin 0
- font-size 75%
- opacity 0.7
-
- > .chart
- height 30px
-
-.mkw-hashtags[data-darkmode]
- root(true)
-
-.mkw-hashtags:not([data-darkmode])
- root(false)
-
-</style>
diff --git a/src/client/app/config.ts b/src/client/app/config.ts
index 74b9ea21c8..a326c521db 100644
--- a/src/client/app/config.ts
+++ b/src/client/app/config.ts
@@ -4,6 +4,7 @@ declare const _THEME_COLOR_: string;
declare const _COPYRIGHT_: string;
declare const _VERSION_: string;
declare const _CODENAME_: string;
+declare const _ENV_: string;
const address = new URL(location.href);
@@ -18,3 +19,4 @@ export const themeColor = _THEME_COLOR_;
export const copyright = _COPYRIGHT_;
export const version = _VERSION_;
export const codename = _CODENAME_;
+export const env = _ENV_;
diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts
index e9d92d1eb1..f08e8a2b4e 100644
--- a/src/client/app/desktop/api/update-avatar.ts
+++ b/src/client/app/desktop/api/update-avatar.ts
@@ -16,7 +16,7 @@ export default (os: OS) => {
text: '%i18n:common.got-it%'
}]
});
- reject();
+ return reject('invalid-filetype');
}
const w = os.new(CropWindow, {
diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts
index e8fa35149b..42c9d69349 100644
--- a/src/client/app/desktop/api/update-banner.ts
+++ b/src/client/app/desktop/api/update-banner.ts
@@ -16,7 +16,7 @@ export default (os: OS) => {
text: '%i18n:common.got-it%'
}]
});
- reject();
+ return reject('invalid-filetype');
}
const w = os.new(CropWindow, {
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
index f0e8a42662..e32682286c 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -6,7 +6,6 @@ import VueRouter from 'vue-router';
// Style
import './style.styl';
-import '../../element.scss';
import init from '../init';
import fuckAdBlock from '../common/scripts/fuck-ad-block';
diff --git a/src/client/app/desktop/views/components/charts.vue b/src/client/app/desktop/views/components/charts.vue
index c4e92e429f..e401095363 100644
--- a/src/client/app/desktop/views/components/charts.vue
+++ b/src/client/app/desktop/views/components/charts.vue
@@ -19,6 +19,11 @@
<option value="drive">%i18n:@charts.drive%</option>
<option value="drive-total">%i18n:@charts.drive-total%</option>
</optgroup>
+ <optgroup label="%i18n:@network%">
+ <option value="network-requests">%i18n:@charts.network-requests%</option>
+ <option value="network-time">%i18n:@charts.network-time%</option>
+ <option value="network-usage">%i18n:@charts.network-usage%</option>
+ </optgroup>
</select>
<div>
<span @click="span = 'day'" :class="{ active: span == 'day' }">%i18n:@per-day%</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">%i18n:@per-hour%</span>
@@ -41,7 +46,10 @@ const colors = {
localPlus: 'rgb(52, 178, 118)',
remotePlus: 'rgb(158, 255, 209)',
localMinus: 'rgb(255, 97, 74)',
- remoteMinus: 'rgb(255, 149, 134)'
+ remoteMinus: 'rgb(255, 149, 134)',
+
+ incoming: 'rgb(52, 178, 118)',
+ outgoing: 'rgb(255, 97, 74)',
};
const rgba = (color: string): string => {
@@ -75,6 +83,9 @@ export default Vue.extend({
case 'drive-total': return this.driveTotalChart();
case 'drive-files': return this.driveFilesChart();
case 'drive-files-total': return this.driveFilesTotalChart();
+ case 'network-requests': return this.networkRequestsChart();
+ case 'network-time': return this.networkTimeChart();
+ case 'network-usage': return this.networkUsageChart();
}
},
@@ -89,7 +100,7 @@ export default Vue.extend({
created() {
(this as any).api('chart', {
- limit: 32
+ limit: 35
}).then(chart => {
this.chart = chart;
});
@@ -544,7 +555,95 @@ export default Vue.extend({
}
}
}];
- }
+ },
+
+ networkRequestsChart(): any {
+ const data = this.stats.slice().reverse().map(x => ({
+ date: new Date(x.date),
+ requests: x.network.requests
+ }));
+
+ return [{
+ datasets: [{
+ label: 'Requests',
+ fill: true,
+ backgroundColor: rgba(colors.localPlus),
+ borderColor: colors.localPlus,
+ borderWidth: 2,
+ pointBackgroundColor: '#fff',
+ lineTension: 0,
+ data: data.map(x => ({ t: x.date, y: x.requests }))
+ }]
+ }];
+ },
+
+ networkTimeChart(): any {
+ const data = this.stats.slice().reverse().map(x => ({
+ date: new Date(x.date),
+ time: x.network.requests != 0 ? (x.network.totalTime / x.network.requests) : 0,
+ }));
+
+ return [{
+ datasets: [{
+ label: 'Avg time (ms)',
+ fill: true,
+ backgroundColor: rgba(colors.localPlus),
+ borderColor: colors.localPlus,
+ borderWidth: 2,
+ pointBackgroundColor: '#fff',
+ lineTension: 0,
+ data: data.map(x => ({ t: x.date, y: x.time }))
+ }]
+ }];
+ },
+
+ networkUsageChart(): any {
+ const data = this.stats.slice().reverse().map(x => ({
+ date: new Date(x.date),
+ incoming: x.network.incomingBytes,
+ outgoing: x.network.outgoingBytes
+ }));
+
+ return [{
+ datasets: [{
+ label: 'Incoming',
+ fill: true,
+ backgroundColor: rgba(colors.incoming),
+ borderColor: colors.incoming,
+ borderWidth: 2,
+ pointBackgroundColor: '#fff',
+ lineTension: 0,
+ data: data.map(x => ({ t: x.date, y: x.incoming }))
+ }, {
+ label: 'Outgoing',
+ fill: true,
+ backgroundColor: rgba(colors.outgoing),
+ borderColor: colors.outgoing,
+ borderWidth: 2,
+ pointBackgroundColor: '#fff',
+ lineTension: 0,
+ data: data.map(x => ({ t: x.date, y: x.outgoing }))
+ }]
+ }, {
+ scales: {
+ yAxes: [{
+ ticks: {
+ callback: value => {
+ return Vue.filter('bytes')(value, 1);
+ }
+ }
+ }]
+ },
+ tooltips: {
+ callbacks: {
+ label: (tooltipItem, data) => {
+ const label = data.datasets[tooltipItem.datasetIndex].label || '';
+ return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
+ }
+ }
+ }
+ }];
+ },
}
});
</script>
@@ -582,6 +681,6 @@ export default Vue.extend({
> div
> *
display block
- height 320px
+ height 350px
</style>
diff --git a/src/client/app/desktop/views/components/context-menu.vue b/src/client/app/desktop/views/components/context-menu.vue
index afb6838eb6..49aeac143f 100644
--- a/src/client/app/desktop/views/components/context-menu.vue
+++ b/src/client/app/desktop/views/components/context-menu.vue
@@ -64,7 +64,7 @@ export default Vue.extend({
});
this.$emit('closed');
- this.$destroy();
+ this.destroyDom();
}
}
});
diff --git a/src/client/app/desktop/views/components/dialog.vue b/src/client/app/desktop/views/components/dialog.vue
index aff21c1754..bbb1e0030c 100644
--- a/src/client/app/desktop/views/components/dialog.vue
+++ b/src/client/app/desktop/views/components/dialog.vue
@@ -78,7 +78,7 @@ export default Vue.extend({
scale: 0.8,
duration: 300,
easing: [ 0.5, -0.5, 1, 0.5 ],
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
},
onBgClick() {
diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue
index 83880fef5c..e6b71f9426 100644
--- a/src/client/app/desktop/views/components/drive.folder.vue
+++ b/src/client/app/desktop/views/components/drive.folder.vue
@@ -163,7 +163,7 @@ export default Vue.extend({
});
break;
default:
- alert('%i18n:@unhandled-error% ' + err);
+ alert(`%i18n:@unhandled-error% ${err}`);
}
});
}
diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue
index d919e4a5ea..cb289027d4 100644
--- a/src/client/app/desktop/views/components/drive.vue
+++ b/src/client/app/desktop/views/components/drive.vue
@@ -323,7 +323,7 @@ export default Vue.extend({
});
break;
default:
- alert('%i18n:@unhandled-error% ' + err);
+ alert(`%i18n:@unhandled-error% ${err}`);
}
});
}
@@ -404,7 +404,7 @@ export default Vue.extend({
folder: folder
});
} else {
- window.open(url + '/i/drive/folder/' + folder.id,
+ window.open(`${url}/i/drive/folder/${folder.id}`,
'drive_window',
'height=500, width=800');
}
diff --git a/src/client/app/desktop/views/components/follow-button.vue b/src/client/app/desktop/views/components/follow-button.vue
index 62742a8f39..1db4b0cfa4 100644
--- a/src/client/app/desktop/views/components/follow-button.vue
+++ b/src/client/app/desktop/views/components/follow-button.vue
@@ -55,13 +55,15 @@ export default Vue.extend({
methods: {
onFollow(user) {
if (user.id == this.u.id) {
- this.user.isFollowing = user.isFollowing;
+ this.u.isFollowing = user.isFollowing;
+ this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
}
},
onUnfollow(user) {
if (user.id == this.u.id) {
- this.user.isFollowing = user.isFollowing;
+ this.u.isFollowing = user.isFollowing;
+ this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
}
},
@@ -74,7 +76,7 @@ export default Vue.extend({
userId: this.u.id
});
} else {
- if (this.u.isLocked && this.u.hasPendingFollowRequestFromYou) {
+ if (this.u.hasPendingFollowRequestFromYou) {
this.u = await (this as any).api('following/requests/cancel', {
userId: this.u.id
});
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index 7dfd9e4359..4e8a212b00 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -14,7 +14,7 @@
<p class="empty" v-if="!fetching && users.length == 0">%i18n:@empty%</p>
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@fetching%<mk-ellipsis/></p>
<a class="refresh" @click="refresh">%i18n:@refresh%</a>
- <button class="close" @click="$destroy()" title="%i18n:@close%">%fa:times%</button>
+ <button class="close" @click="destroyDom()" title="%i18n:@close%">%fa:times%</button>
</div>
</template>
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 026522d907..89a340d3ae 100644
--- a/src/client/app/desktop/views/components/media-image-dialog.vue
+++ b/src/client/app/desktop/views/components/media-image-dialog.vue
@@ -26,7 +26,7 @@ export default Vue.extend({
opacity: 0,
duration: 100,
easing: 'linear',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}
}
diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue
index 8b68f260fa..f9ab188ca5 100644
--- a/src/client/app/desktop/views/components/media-image.vue
+++ b/src/client/app/desktop/views/components/media-image.vue
@@ -1,5 +1,5 @@
<template>
-<div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide" @click="hide = false">
+<div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
<div>
<b>%fa:exclamation-triangle% %i18n:@sensitive%</b>
<span>%i18n:@click-to-show%</span>
@@ -27,12 +27,13 @@ export default Vue.extend({
},
raw: {
default: false
- },
- hide: {
- type: Boolean,
- default: true
}
},
+ data() {
+ return {
+ hide: true
+ };
+ },
computed: {
style(): any {
return {
@@ -48,7 +49,7 @@ export default Vue.extend({
const mouseY = e.clientY - rect.top;
const xp = mouseX / this.$el.offsetWidth * 100;
const yp = mouseY / this.$el.offsetHeight * 100;
- this.$el.style.backgroundPosition = xp + '% ' + yp + '%';
+ this.$el.style.backgroundPosition = `${xp}% ${yp}%`;
this.$el.style.backgroundImage = `url("${this.image.url}")`;
},
@@ -89,7 +90,7 @@ export default Vue.extend({
text-align center
font-size 12px
- > b
+ > *
display block
</style>
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 959cefa42c..03c93c8939 100644
--- a/src/client/app/desktop/views/components/media-video-dialog.vue
+++ b/src/client/app/desktop/views/components/media-video-dialog.vue
@@ -28,7 +28,7 @@ export default Vue.extend({
opacity: 0,
duration: 100,
easing: 'linear',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}
}
diff --git a/src/client/app/desktop/views/components/media-video.vue b/src/client/app/desktop/views/components/media-video.vue
index 6c60f2da96..1ff762abc2 100644
--- a/src/client/app/desktop/views/components/media-video.vue
+++ b/src/client/app/desktop/views/components/media-video.vue
@@ -36,12 +36,13 @@ export default Vue.extend({
},
inlinePlayable: {
default: false
- },
- hide: {
- type: Boolean,
- default: true
}
},
+ data() {
+ return {
+ hide: true
+ };
+ },
computed: {
imageStyle(): any {
return {
@@ -79,7 +80,6 @@ export default Vue.extend({
justify-content center
align-items center
font-size 3.5em
-
cursor zoom-in
overflow hidden
background-position center
@@ -101,5 +101,4 @@ export default Vue.extend({
> b
display block
-
</style>
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index 1ba4a9a447..7307eeb7dc 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -37,20 +37,26 @@
</router-link>
</header>
<div class="body">
- <div class="text">
- <span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
- <span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
- <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
- </div>
- <div class="media" v-if="p.media.length > 0">
- <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" :detail="true"/>
- <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
- <div class="map" v-if="p.geo" ref="map"></div>
- <div class="renote" v-if="p.renote">
- <mk-note-preview :note="p.renote"/>
+ <p v-if="p.cw != null" class="cw">
+ <span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="p.cw == null || showContent">
+ <div class="text">
+ <span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
+ <span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
+ <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
+ </div>
+ <div class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files" :raw="true"/>
+ </div>
+ <mk-poll v-if="p.poll" :note="p"/>
+ <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
+ <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
+ <div class="map" v-if="p.geo" ref="map"></div>
+ <div class="renote" v-if="p.renote">
+ <mk-note-preview :note="p.renote"/>
+ </div>
</div>
</div>
<footer>
@@ -86,6 +92,7 @@ import MkRenoteFormWindow from './renote-form-window.vue';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './notes.note.sub.vue';
+import { sum } from '../../../../../prelude/array';
export default Vue.extend({
components: {
@@ -104,6 +111,7 @@ export default Vue.extend({
data() {
return {
+ showContent: false,
conversation: [],
conversationFetching: false,
replies: []
@@ -114,22 +122,24 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.length == 0 &&
this.note.poll == null);
},
+
p(): any {
return this.isRenote ? this.note.renote : this.note;
},
+
reactionsCount(): number {
return this.p.reactionCounts
- ? Object.keys(this.p.reactionCounts)
- .map(key => this.p.reactionCounts[key])
- .reduce((a, b) => a + b)
+ ? sum(Object.values(this.p.reactionCounts))
: 0;
},
+
title(): string {
return new Date(this.p.createdAt).toLocaleString();
},
+
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
@@ -184,22 +194,26 @@ export default Vue.extend({
this.conversation = conversation.reverse();
});
},
+
reply() {
(this as any).os.new(MkPostFormWindow, {
reply: this.p
});
},
+
renote() {
(this as any).os.new(MkRenoteFormWindow, {
note: this.p
});
},
+
react() {
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
note: this.p
});
},
+
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
@@ -327,37 +341,49 @@ root(isDark)
> .body
padding 8px 0
- > .text
+ > .cw
cursor default
display block
margin 0
padding 0
overflow-wrap break-word
- font-size 1.5em
color isDark ? #fff : #717171
- > .renote
- margin 8px 0
+ > .text
+ margin-right 8px
+
+ > .content
+ > .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
- > .mk-note-preview
- padding 16px
- border dashed 1px #c0dac6
- border-radius 8px
+ > *
+ padding 16px
+ border dashed 1px #c0dac6
+ border-radius 8px
- > .location
- margin 4px 0
- font-size 12px
- color #ccc
+ > .location
+ margin 4px 0
+ font-size 12px
+ color #ccc
- > .map
- width 100%
- height 300px
+ > .map
+ width 100%
+ height 300px
- &:empty
- display none
+ &:empty
+ display none
- > .mk-url-preview
- margin-top 8px
+ > .mk-url-preview
+ margin-top 8px
> footer
font-size 1.2em
diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue
index c723db98c0..6c84165356 100644
--- a/src/client/app/desktop/views/components/note-preview.vue
+++ b/src/client/app/desktop/views/components/note-preview.vue
@@ -1,10 +1,16 @@
<template>
-<div class="mk-note-preview" :title="title">
+<div class="qiziqtywpuaucsgarwajitwaakggnisj" :title="title">
<mk-avatar class="avatar" :user="note.user" v-if="!mini"/>
<div class="main">
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
- <mk-sub-note-content class="text" :note="note"/>
+ <p v-if="note.cw != null" class="cw">
+ <span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="note.cw == null || showContent">
+ <mk-sub-note-content class="text" :note="note"/>
+ </div>
</div>
</div>
</div>
@@ -25,6 +31,13 @@ export default Vue.extend({
default: false
}
},
+
+ data() {
+ return {
+ showContent: false
+ };
+ },
+
computed: {
title(): string {
return new Date(this.note.createdAt).toLocaleString();
@@ -52,16 +65,28 @@ root(isDark)
> .body
- > .text
+ > .cw
cursor default
+ display block
margin 0
padding 0
- color isDark ? #959ba7 : #717171
+ overflow-wrap break-word
+ color isDark ? #fff : #717171
+
+ > .text
+ margin-right 8px
+
+ > .content
+ > .text
+ cursor default
+ margin 0
+ padding 0
+ color isDark ? #959ba7 : #717171
-.mk-note-preview[data-darkmode]
+.qiziqtywpuaucsgarwajitwaakggnisj[data-darkmode]
root(true)
-.mk-note-preview:not([data-darkmode])
+.qiziqtywpuaucsgarwajitwaakggnisj: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 fc851e83e9..8f01ddd43c 100644
--- a/src/client/app/desktop/views/components/notes.note.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -1,10 +1,16 @@
<template>
-<div class="sub" :title="title">
+<div class="tkfdzaxtkdeianobciwadajxzbddorql" :title="title">
<mk-avatar class="avatar" :user="note.user"/>
<div class="main">
<mk-note-header class="header" :note="note"/>
<div class="body">
- <mk-sub-note-content class="text" :note="note"/>
+ <p v-if="note.cw != null" class="cw">
+ <span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="note.cw == null || showContent">
+ <mk-sub-note-content class="text" :note="note"/>
+ </div>
</div>
</div>
</div>
@@ -14,7 +20,19 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['note'],
+ props: {
+ note: {
+ type: Object,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ showContent: false
+ };
+ },
+
computed: {
title(): string {
return new Date(this.note.createdAt).toLocaleString();
@@ -48,20 +66,32 @@ root(isDark)
> .body
- > .text
+ > .cw
cursor default
+ display block
margin 0
padding 0
- color isDark ? #959ba7 : #717171
+ overflow-wrap break-word
+ color isDark ? #fff : #717171
+
+ > .text
+ margin-right 8px
+
+ > .content
+ > .text
+ cursor default
+ margin 0
+ padding 0
+ color isDark ? #959ba7 : #717171
- pre
- max-height 120px
- font-size 80%
+ pre
+ max-height 120px
+ font-size 80%
-.sub[data-darkmode]
+.tkfdzaxtkdeianobciwadajxzbddorql[data-darkmode]
root(true)
-.sub:not([data-darkmode])
+.tkfdzaxtkdeianobciwadajxzbddorql: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 7592ae3905..46a866f9a7 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -18,7 +18,7 @@
<div class="body">
<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 ? '%i18n:@hide%' : '%i18n:@see-more%' }}</span>
+ <mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
@@ -28,15 +28,13 @@
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.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 class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<a class="location" v-if="p.geo" :href="`https://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 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>
</div>
@@ -78,6 +76,7 @@ import MkRenoteFormWindow from './renote-form-window.vue';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './notes.note.sub.vue';
+import { sum } from '../../../../../prelude/array';
function focus(el, fn) {
const target = fn(el);
@@ -95,7 +94,12 @@ export default Vue.extend({
XSub
},
- props: ['note'],
+ props: {
+ note: {
+ type: Object,
+ required: true
+ }
+ },
data() {
return {
@@ -110,7 +114,7 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.length == 0 &&
this.note.poll == null);
},
@@ -120,9 +124,7 @@ export default Vue.extend({
reactionsCount(): number {
return this.p.reactionCounts
- ? Object.keys(this.p.reactionCounts)
- .map(key => this.p.reactionCounts[key])
- .reduce((a, b) => a + b)
+ ? sum(Object.values(this.p.reactionCounts))
: 0;
},
@@ -399,19 +401,6 @@ root(isDark)
> .text
margin-right 8px
- > .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
-
- &:hover
- background isDark ? #707b97 : #bbc4ce
-
> .content
> .text
@@ -470,7 +459,7 @@ root(isDark)
> .renote
margin 8px 0
- > .mk-note-preview
+ > *
padding 16px
border dashed 1px isDark ? #4e945e : #c0dac6
border-radius 8px
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index a1c1207a7b..ec9aa285d0 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -10,8 +10,7 @@
</div>
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!--<transition-group name="mk-notes" class="transition">-->
- <div class="notes">
+ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div">
<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">
@@ -19,8 +18,7 @@
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!--</transition-group>-->
+ </component>
<footer v-if="more">
<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
@@ -122,7 +120,7 @@ export default Vue.extend({
prepend(note, silent = false) {
//#region 弾く
const isMyNote = note.userId == this.$store.state.i.id;
- const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
+ const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
if (this.$store.state.settings.showMyRenotes === false) {
if (isMyNote && isPureRenote) {
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index bfe71903e4..2eb80dcd01 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -2,8 +2,7 @@
<div class="mk-notifications">
<div class="notifications" v-if="notifications.length != 0">
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!-- <transition-group name="mk-notifications" class="transition"> -->
- <div>
+ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div">
<template v-for="(notification, i) in _notifications">
<div class="notification" :class="notification.type" :key="notification.id">
<mk-time :time="notification.createdAt"/>
@@ -97,8 +96,7 @@
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!-- </transition-group> -->
+ </component>
</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%' }}
diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue
index 51a416e281..a88c96d1bf 100644
--- a/src/client/app/desktop/views/components/post-form-window.vue
+++ b/src/client/app/desktop/views/components/post-form-window.vue
@@ -4,7 +4,7 @@
<span class="icon" v-if="geo">%fa:map-marker-alt%</span>
<span v-if="!reply">%i18n:@note%</span>
<span v-if="reply">%i18n:@reply%</span>
- <span class="count" v-if="media.length != 0">{{ '%i18n:@attaches%'.replace('{}', media.length) }}</span>
+ <span class="count" v-if="files.length != 0">{{ '%i18n:@attaches%'.replace('{}', files.length) }}</span>
<span class="count" v-if="uploadings.length != 0">{{ '%i18n:@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
</span>
@@ -14,7 +14,7 @@
:reply="reply"
@posted="onPosted"
@change-uploadings="onChangeUploadings"
- @change-attached-media="onChangeMedia"
+ @change-attached-files="onChangeFiles"
@geo-attached="onGeoAttached"
@geo-dettached="onGeoDettached"/>
</div>
@@ -29,7 +29,7 @@ export default Vue.extend({
data() {
return {
uploadings: [],
- media: [],
+ files: [],
geo: null
};
},
@@ -42,8 +42,8 @@ export default Vue.extend({
onChangeUploadings(files) {
this.uploadings = files;
},
- onChangeMedia(media) {
- this.media = media;
+ onChangeFiles(files) {
+ this.files = files;
},
onGeoAttached(geo) {
this.geo = geo;
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index bacaea65ee..8db85aeaca 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -20,7 +20,7 @@
@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
v-autocomplete="'text'"
></textarea>
- <div class="medias" :class="{ with: poll }" v-show="files.length != 0">
+ <div class="files" :class="{ with: poll }" v-show="files.length != 0">
<x-draggable :list="files" :options="{ animation: 150 }">
<div v-for="file in files" :key="file.id">
<div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div>
@@ -35,7 +35,7 @@
<button class="upload" title="%i18n:@attach-media-from-local%" @click="chooseFile">%fa:upload%</button>
<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="%i18n:@create-poll%" @click="poll = !poll">%fa:chart-pie%</button>
<button class="poll" title="%i18n:@hide-contents%" @click="useCw = !useCw">%fa:eye-slash%</button>
<button class="geo" title="%i18n:@attach-location-information%" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button>
<button class="visibility" title="%i18n:@visibility%" @click="setVisibility" ref="visibilityButton">
@@ -45,11 +45,11 @@
<span v-if="visibility === 'specified'">%fa:envelope%</span>
<span v-if="visibility === 'private'">%fa:lock%</span>
</button>
- <p class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</p>
+ <p class="text-count" :class="{ over: this.trimmedLength(text) > 1000 }">{{ 1000 - this.trimmedLength(text) }}</p>
<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
{{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
</button>
- <input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
+ <input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
<div class="dropzone" v-if="draghover"></div>
</div>
</template>
@@ -62,6 +62,9 @@ import getFace from '../../../common/scripts/get-face';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import parse from '../../../../../mfm/parse';
import { host } from '../../../config';
+import { erase, unique } from '../../../../../prelude/array';
+import { length } from 'stringz';
+import parseAcct from '../../../../../misc/acct/parse';
export default Vue.extend({
components: {
@@ -99,7 +102,7 @@ export default Vue.extend({
useCw: false,
cw: null,
geo: null,
- visibility: this.$store.state.device.visibility || 'public',
+ visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility,
visibleUsers: [],
autocomplete: null,
draghover: false,
@@ -110,9 +113,9 @@ export default Vue.extend({
computed: {
draftId(): string {
return this.renote
- ? 'renote:' + this.renote.id
+ ? `renote:${this.renote.id}`
: this.reply
- ? 'reply:' + this.reply.id
+ ? `reply:${this.reply.id}`
: 'note';
},
@@ -145,7 +148,7 @@ export default Vue.extend({
canPost(): boolean {
return !this.posting &&
(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) &&
- (this.text.trim().length <= 1000);
+ (length(this.text.trim()) <= 1000);
}
},
@@ -188,7 +191,7 @@ export default Vue.extend({
(this.$refs.poll as any).set(draft.data.poll);
});
}
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
}
}
@@ -197,6 +200,10 @@ export default Vue.extend({
},
methods: {
+ trimmedLength(text: string) {
+ return length(text.trim());
+ },
+
addTag(tag: string) {
insertTextAtCursor(this.$refs.text, ` #${tag} `);
},
@@ -225,12 +232,12 @@ export default Vue.extend({
attachMedia(driveFile) {
this.files.push(driveFile);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
detachMedia(id) {
this.files = this.files.filter(x => x.id != id);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
onChangeFile() {
@@ -249,7 +256,7 @@ export default Vue.extend({
this.text = '';
this.files = [];
this.poll = false;
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
onKeydown(e) {
@@ -297,7 +304,7 @@ export default Vue.extend({
if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile);
this.files.push(file);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
e.preventDefault();
}
//#endregion
@@ -313,7 +320,7 @@ export default Vue.extend({
this.geo = pos.coords;
this.$emit('geo-attached', this.geo);
}, err => {
- alert('%i18n:@error%: ' + err.message);
+ alert(`%i18n:@error%: ${err.message}`);
}, {
enableHighAccuracy: true
});
@@ -336,17 +343,16 @@ export default Vue.extend({
addVisibleUser() {
(this as any).apis.input({
title: '%i18n:@enter-username%'
- }).then(username => {
- (this as any).api('users/show', {
- username
- }).then(user => {
+ }).then(acct => {
+ if (acct.startsWith('@')) acct = acct.substr(1);
+ (this as any).api('users/show', parseAcct(acct)).then(user => {
this.visibleUsers.push(user);
});
});
},
removeVisibleUser(user) {
- this.visibleUsers = this.visibleUsers.filter(u => u != user);
+ this.visibleUsers = erase(user, this.visibleUsers);
},
post() {
@@ -354,7 +360,7 @@ export default Vue.extend({
(this as any).api('notes/create', {
text: this.text == '' ? undefined : this.text,
- mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
+ fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined,
renoteId: this.renote ? this.renote.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
@@ -391,7 +397,7 @@ export default Vue.extend({
if (this.text && this.text != '') {
const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
- localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], [])));
+ localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}
},
@@ -514,7 +520,7 @@ root(isDark)
margin-right 8px
white-space nowrap
- > .medias
+ > .files
margin 0
padding 0
background isDark ? #181b23 : lighten($theme-color, 98%)
diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue
index 38eab3362f..c5192ecaac 100644
--- a/src/client/app/desktop/views/components/renote-form.vue
+++ b/src/client/app/desktop/views/components/renote-form.vue
@@ -1,6 +1,6 @@
<template>
<div class="mk-renote-form">
- <mk-note-preview :note="note"/>
+ <mk-note-preview class="preview" :note="note"/>
<template v-if="!quote">
<footer>
<a class="quote" v-if="!quote" @click="onQuote">%i18n:@quote%</a>
@@ -61,7 +61,7 @@ export default Vue.extend({
root(isDark)
- > .mk-note-preview
+ > .preview
margin 16px 22px
> footer
diff --git a/src/client/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue
index deb865b102..b4cc570282 100644
--- a/src/client/app/desktop/views/components/settings-window.vue
+++ b/src/client/app/desktop/views/components/settings-window.vue
@@ -1,13 +1,19 @@
<template>
<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy">
<span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span>
- <mk-settings @done="close"/>
+ <mk-settings :initial-page="initialPage" @done="close"/>
</mk-window>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
+ props: {
+ initialPage: {
+ type: String,
+ required: false
+ }
+ },
methods: {
close() {
(this as any).$refs.window.close();
diff --git a/src/client/app/desktop/views/components/settings.drive.vue b/src/client/app/desktop/views/components/settings.drive.vue
index e8a3cc9685..d254b27110 100644
--- a/src/client/app/desktop/views/components/settings.drive.vue
+++ b/src/client/app/desktop/views/components/settings.drive.vue
@@ -1,7 +1,6 @@
<template>
<div class="root">
<template v-if="!fetching">
- <el-progress :text-inside="true" :stroke-width="18" :percentage="Math.floor((usage / capacity) * 100)"/>
<p><b>{{ capacity | bytes }}</b>%i18n:max%<b>{{ usage | bytes }}</b>%i18n:in-use%</p>
</template>
</div>
diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue
index 262583b640..d47b5b224b 100644
--- a/src/client/app/desktop/views/components/settings.profile.vue
+++ b/src/client/app/desktop/views/components/settings.profile.vue
@@ -19,7 +19,7 @@
</label>
<label class="ui from group">
<p>%i18n:@birthday%</p>
- <el-date-picker v-model="birthday" type="date" value-format="yyyy-MM-dd"/>
+ <input type="date" v-model="birthday"/>
</label>
<button class="ui primary" @click="save">%i18n:@save%</button>
<section>
@@ -30,6 +30,7 @@
<h2>%i18n:@other%</h2>
<mk-switch v-model="$store.state.i.isBot" @change="onChangeIsBot" text="%i18n:@is-bot%"/>
<mk-switch v-model="$store.state.i.isCat" @change="onChangeIsCat" text="%i18n:@is-cat%"/>
+ <mk-switch v-model="alwaysMarkNsfw" text="%i18n:common.always-mark-nsfw%"/>
</section>
</div>
</template>
@@ -46,6 +47,12 @@ export default Vue.extend({
birthday: null,
};
},
+ computed: {
+ alwaysMarkNsfw: {
+ get() { return this.$store.state.i.settings.alwaysMarkNsfw; },
+ set(value) { (this as any).api('i/update', { alwaysMarkNsfw: value }); }
+ },
+ },
created() {
this.name = this.$store.state.i.name || '';
this.location = this.$store.state.i.profile.location;
diff --git a/src/client/app/desktop/views/components/settings.tags.vue b/src/client/app/desktop/views/components/settings.tags.vue
new file mode 100644
index 0000000000..a7234f7d87
--- /dev/null
+++ b/src/client/app/desktop/views/components/settings.tags.vue
@@ -0,0 +1,65 @@
+<template>
+<div class="vfcitkilproprqtbnpoertpsziierwzi">
+ <div v-for="timeline in timelines" class="timeline">
+ <ui-input v-model="timeline.title" @change="save">
+ <span>%i18n:@title%</span>
+ </ui-input>
+ <ui-textarea :value="timeline.query ? timeline.query.map(tags => tags.join(' ')).join('\n') : ''" @input="onQueryChange(timeline, $event)">
+ <span>%i18n:@query%</span>
+ </ui-textarea>
+ <ui-button class="save" @click="save">%i18n:@save%</ui-button>
+ </div>
+ <ui-button class="add" @click="add">%i18n:@add%</ui-button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as uuid from 'uuid';
+
+export default Vue.extend({
+ data() {
+ return {
+ timelines: this.$store.state.settings.tagTimelines
+ };
+ },
+
+ methods: {
+ add() {
+ this.timelines.push({
+ id: uuid(),
+ title: '',
+ query: ''
+ });
+
+ this.save();
+ },
+
+ save() {
+ this.$store.dispatch('settings/set', { key: 'tagTimelines', value: this.timelines });
+ },
+
+ onQueryChange(timeline, value) {
+ timeline.query = value.split('\n').map(tags => tags.split(' '));
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+
+root(isDark)
+ > .timeline
+ padding-bottom 16px
+ border-bottom solid 1px rgba(#000, 0.1)
+
+ > .add
+ margin-top 16px
+
+.vfcitkilproprqtbnpoertpsziierwzi[data-darkmode]
+ root(true)
+
+.vfcitkilproprqtbnpoertpsziierwzi:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 7d6f1d55fb..312a7ed56e 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -5,6 +5,7 @@
<p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p>
<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p>
<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:@drive%</p>
+ <p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'">%fa:hashtag .fw%%i18n:@tags%</p>
<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p>
<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p>
<p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p>
@@ -20,12 +21,28 @@
<section class="web" v-show="page == 'web'">
<h1>%i18n:@behaviour%</h1>
- <mk-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll" text="%i18n:@fetch-on-scroll%">
+ <mk-switch v-model="fetchOnScroll" text="%i18n:@fetch-on-scroll%">
<span>%i18n:@fetch-on-scroll-desc%</span>
</mk-switch>
<mk-switch v-model="autoPopout" text="%i18n:@auto-popout%">
<span>%i18n:@auto-popout-desc%</span>
</mk-switch>
+
+ <section>
+ <header>%i18n:@note-visibility%</header>
+ <mk-switch v-model="rememberNoteVisibility" text="%i18n:@remember-note-visibility%"/>
+ <section>
+ <header>%i18n:@default-note-visibility%</header>
+ <ui-select v-model="defaultNoteVisibility">
+ <option value="public">%i18n:common.note-visibility.public%</option>
+ <option value="home">%i18n:common.note-visibility.home%</option>
+ <option value="followers">%i18n:common.note-visibility.followers%</option>
+ <option value="specified">%i18n:common.note-visibility.specified%</option>
+ <option value="private">%i18n:common.note-visibility.private%</option>
+ </ui-select>
+ </section>
+ </section>
+
<details>
<summary>%i18n:@advanced%</summary>
<mk-switch v-model="apiViaStream" text="%i18n:@api-via-stream%">
@@ -43,23 +60,27 @@
<button class="ui" @click="updateWallpaper">%i18n:@choose-wallpaper%</button>
<button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button>
<mk-switch v-model="darkmode" text="%i18n:@dark-mode%"/>
- <mk-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons" text="%i18n:@circle-icons%"/>
- <mk-switch v-model="$store.state.settings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="%i18n:@gradient-window-header%"/>
- <mk-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi" text="%i18n:common.i-like-sushi%"/>
+ <mk-switch v-model="circleIcons" text="%i18n:@circle-icons%"/>
+ <mk-switch v-model="reduceMotion" text="%i18n:common.reduce-motion%"/>
+ <mk-switch v-model="contrastedAcct" text="%i18n:@contrasted-acct%"/>
+ <mk-switch v-model="showFullAcct" text="%i18n:common.show-full-acct%"/>
+ <mk-switch v-model="gradientWindowHeader" text="%i18n:@gradient-window-header%"/>
+ <mk-switch v-model="iLikeSushi" text="%i18n:common.i-like-sushi%"/>
</div>
- <mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/>
- <mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/>
- <mk-switch v-model="$store.state.settings.showClockOnHeader" @change="onChangeShowClockOnHeader" text="%i18n:@show-clock-on-header%"/>
- <mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/>
- <mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/>
- <mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/>
- <mk-switch v-model="$store.state.settings.showLocalRenotes" @change="onChangeShowLocalRenotes" text="%i18n:@show-local-renotes%"/>
- <mk-switch v-model="$store.state.settings.showMaps" @change="onChangeShowMaps" text="%i18n:@show-maps%">
+ <mk-switch v-model="showPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/>
+ <mk-switch v-model="suggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/>
+ <mk-switch v-model="showClockOnHeader" text="%i18n:@show-clock-on-header%"/>
+ <mk-switch v-model="alwaysShowNsfw" text="%i18n:common.always-show-nsfw%"/>
+ <mk-switch v-model="showReplyTarget" text="%i18n:@show-reply-target%"/>
+ <mk-switch v-model="showMyRenotes" text="%i18n:@show-my-renotes%"/>
+ <mk-switch v-model="showRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/>
+ <mk-switch v-model="showLocalRenotes" text="%i18n:@show-local-renotes%"/>
+ <mk-switch v-model="showMaps" text="%i18n:@show-maps%">
<span>%i18n:@show-maps-desc%</span>
</mk-switch>
- <mk-switch v-model="$store.state.settings.disableAnimatedMfm" @change="onChangeDisableAnimatedMfm" text="%i18n:common.disable-animated-mfm%"/>
- <mk-switch v-model="$store.state.settings.games.reversi.showBoardLabels" @change="onChangeReversiBoardLabels" text="%i18n:common.show-reversi-board-labels%"/>
- <mk-switch v-model="$store.state.settings.games.reversi.useContrastStones" @change="onChangeUseContrastReversiStones" text="%i18n:common.use-contrast-reversi-stones%"/>
+ <mk-switch v-model="disableAnimatedMfm" text="%i18n:common.disable-animated-mfm%"/>
+ <mk-switch v-model="games_reversi_showBoardLabels" text="%i18n:common.show-reversi-board-labels%"/>
+ <mk-switch v-model="games_reversi_useContrastStones" text="%i18n:common.use-contrast-reversi-stones%"/>
</section>
<section class="web" v-show="page == 'web'">
@@ -68,32 +89,31 @@
<span>%i18n:@enable-sounds-desc%</span>
</mk-switch>
<label>%i18n:@volume%</label>
- <el-slider
+ <input type="range"
v-model="soundVolume"
- :show-input="true"
- :format-tooltip="v => `${v * 100}%`"
:disabled="!enableSounds"
- :max="1"
- :step="0.1"
+ max="1"
+ step="0.1"
/>
<button class="ui button" @click="soundTest">%fa:volume-up% %i18n:@test%</button>
</section>
<section class="web" v-show="page == 'web'">
<h1>%i18n:@mobile%</h1>
- <mk-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile" text="%i18n:@disable-via-mobile%"/>
+ <mk-switch v-model="disableViaMobile" text="%i18n:@disable-via-mobile%"/>
</section>
<section class="web" v-show="page == 'web'">
<h1>%i18n:@language%</h1>
- <el-select v-model="lang" placeholder="%i18n:@pick-language%">
- <el-option-group label="%i18n:@recommended%">
- <el-option label="%i18n:@auto%" :value="null"/>
- </el-option-group>
- <el-option-group label="%i18n:@specify-language%">
- <el-option v-for="x in langs" :label="x[1]" :value="x[0]" :key="x[0]"/>
- </el-option-group>
- </el-select>
+ <select v-model="lang" placeholder="%i18n:@pick-language%">
+ <optgroup label="%i18n:@recommended%">
+ <option value="">%i18n:@auto%</option>
+ </optgroup>
+
+ <optgroup label="%i18n:@specify-language%">
+ <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
+ </optgroup>
+ </select>
<div class="none ui info">
<p>%fa:info-circle%%i18n:@language-desc%</p>
</div>
@@ -119,6 +139,11 @@
<x-drive/>
</section>
+ <section class="hashtags" v-show="page == 'hashtags'">
+ <h1>%i18n:@tags%</h1>
+ <x-tags/>
+ </section>
+
<section class="mute" v-show="page == 'mute'">
<h1>%i18n:@mute%</h1>
<x-mute/>
@@ -188,10 +213,6 @@
<mk-switch v-model="enableExperimentalFeatures" text="%i18n:@experimental%">
<span>%i18n:@experimental-desc%</span>
</mk-switch>
- <details v-if="debug">
- <summary>%i18n:@tools%</summary>
- <button class="ui button block" @click="taskmngr">%i18n:@task-manager%</button>
- </details>
</section>
</div>
</div>
@@ -207,9 +228,9 @@ import XApi from './settings.api.vue';
import XApps from './settings.apps.vue';
import XSignins from './settings.signins.vue';
import XDrive from './settings.drive.vue';
+import XTags from './settings.tags.vue';
import { url, langs, version } from '../../../config';
import checkForUpdate from '../../../common/scripts/check-for-update';
-import MkTaskManager from './taskmanager.vue';
export default Vue.extend({
components: {
@@ -220,11 +241,18 @@ export default Vue.extend({
XApi,
XApps,
XSignins,
- XDrive
+ XDrive,
+ XTags
+ },
+ props: {
+ initialPage: {
+ type: String,
+ required: false
+ }
},
data() {
return {
- page: 'profile',
+ page: this.initialPage || 'profile',
meta: null,
version,
langs,
@@ -233,6 +261,11 @@ export default Vue.extend({
};
},
computed: {
+ reduceMotion: {
+ get() { return this.$store.state.device.reduceMotion; },
+ set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
+ },
+
apiViaStream: {
get() { return this.$store.state.device.apiViaStream; },
set(value) { this.$store.commit('device/set', { key: 'apiViaStream', value }); }
@@ -276,7 +309,112 @@ export default Vue.extend({
enableExperimentalFeatures: {
get() { return this.$store.state.device.enableExperimentalFeatures; },
set(value) { this.$store.commit('device/set', { key: 'enableExperimentalFeatures', value }); }
- }
+ },
+
+ alwaysShowNsfw: {
+ get() { return this.$store.state.device.alwaysShowNsfw; },
+ set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); }
+ },
+
+ fetchOnScroll: {
+ get() { return this.$store.state.settings.fetchOnScroll; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); }
+ },
+
+ rememberNoteVisibility: {
+ get() { return this.$store.state.settings.rememberNoteVisibility; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
+ },
+
+ defaultNoteVisibility: {
+ get() { return this.$store.state.settings.defaultNoteVisibility; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
+ },
+
+ showReplyTarget: {
+ get() { return this.$store.state.settings.showReplyTarget; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); }
+ },
+
+ showMyRenotes: {
+ get() { return this.$store.state.settings.showMyRenotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showMyRenotes', value }); }
+ },
+
+ showRenotedMyNotes: {
+ get() { return this.$store.state.settings.showRenotedMyNotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showRenotedMyNotes', value }); }
+ },
+
+ showLocalRenotes: {
+ get() { return this.$store.state.settings.showLocalRenotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showLocalRenotes', value }); }
+ },
+
+ showPostFormOnTopOfTl: {
+ get() { return this.$store.state.settings.showPostFormOnTopOfTl; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showPostFormOnTopOfTl', value }); }
+ },
+
+ suggestRecentHashtags: {
+ get() { return this.$store.state.settings.suggestRecentHashtags; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'suggestRecentHashtags', value }); }
+ },
+
+ showClockOnHeader: {
+ get() { return this.$store.state.settings.showClockOnHeader; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showClockOnHeader', value }); }
+ },
+
+ showMaps: {
+ get() { return this.$store.state.settings.showMaps; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showMaps', value }); }
+ },
+
+ circleIcons: {
+ get() { return this.$store.state.settings.circleIcons; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'circleIcons', value }); }
+ },
+
+ contrastedAcct: {
+ get() { return this.$store.state.settings.contrastedAcct; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'contrastedAcct', value }); }
+ },
+
+ showFullAcct: {
+ get() { return this.$store.state.settings.showFullAcct; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showFullAcct', value }); }
+ },
+
+ iLikeSushi: {
+ get() { return this.$store.state.settings.iLikeSushi; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'iLikeSushi', value }); }
+ },
+
+ games_reversi_showBoardLabels: {
+ get() { return this.$store.state.settings.games.reversi.showBoardLabels; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.showBoardLabels', value }); }
+ },
+
+ games_reversi_useContrastStones: {
+ get() { return this.$store.state.settings.games.reversi.useContrastStones; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.useContrastStones', value }); }
+ },
+
+ disableAnimatedMfm: {
+ get() { return this.$store.state.settings.disableAnimatedMfm; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); }
+ },
+
+ disableViaMobile: {
+ get() { return this.$store.state.settings.disableViaMobile; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'disableViaMobile', value }); }
+ },
+
+ gradientWindowHeader: {
+ get() { return this.$store.state.settings.gradientWindowHeader; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'gradientWindowHeader', value }); }
+ },
},
created() {
(this as any).os.getMeta().then(meta => {
@@ -284,9 +422,6 @@ export default Vue.extend({
});
},
methods: {
- taskmngr() {
- (this as any).os.new(MkTaskManager);
- },
customizeHome() {
this.$router.push('/i/customize-home');
this.$emit('done');
@@ -305,113 +440,11 @@ export default Vue.extend({
wallpaperId: null
});
},
- onChangeFetchOnScroll(v) {
- this.$store.dispatch('settings/set', {
- key: 'fetchOnScroll',
- value: v
- });
- },
onChangeAutoWatch(v) {
(this as any).api('i/update', {
autoWatch: v
});
},
- onChangeDark(v) {
- this.$store.dispatch('settings/set', {
- key: 'dark',
- value: v
- });
- },
- onChangeShowPostFormOnTopOfTl(v) {
- this.$store.dispatch('settings/set', {
- key: 'showPostFormOnTopOfTl',
- value: v
- });
- },
- onChangeSuggestRecentHashtags(v) {
- this.$store.dispatch('settings/set', {
- key: 'suggestRecentHashtags',
- value: v
- });
- },
- onChangeShowClockOnHeader(v) {
- this.$store.dispatch('settings/set', {
- key: 'showClockOnHeader',
- 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
- });
- },
- onChangeShowLocalRenotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showLocalRenotes',
- value: v
- });
- },
- onChangeShowMaps(v) {
- this.$store.dispatch('settings/set', {
- key: 'showMaps',
- value: v
- });
- },
- onChangeCircleIcons(v) {
- this.$store.dispatch('settings/set', {
- key: 'circleIcons',
- value: v
- });
- },
- onChangeILikeSushi(v) {
- this.$store.dispatch('settings/set', {
- key: 'iLikeSushi',
- value: v
- });
- },
- onChangeReversiBoardLabels(v) {
- this.$store.dispatch('settings/set', {
- key: 'games.reversi.showBoardLabels',
- value: v
- });
- },
- onChangeUseContrastReversiStones(v) {
- this.$store.dispatch('settings/set', {
- key: 'games.reversi.useContrastStones',
- value: v
- });
- },
- onChangeDisableAnimatedMfm(v) {
- this.$store.dispatch('settings/set', {
- key: 'disableAnimatedMfm',
- value: v
- });
- },
- onChangeGradientWindowHeader(v) {
- this.$store.dispatch('settings/set', {
- key: 'gradientWindowHeader',
- value: v
- });
- },
- onChangeDisableViaMobile(v) {
- this.$store.dispatch('settings/set', {
- key: 'disableViaMobile',
- value: v
- });
- },
checkForUpdate() {
this.checkingForUpdate = true;
checkForUpdate((this as any).os, true, true).then(newer => {
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 cb0374b910..6889dc231e 100644
--- a/src/client/app/desktop/views/components/sub-note-content.vue
+++ b/src/client/app/desktop/views/components/sub-note-content.vue
@@ -7,9 +7,9 @@
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
<a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RP: ...</a>
</div>
- <details v-if="note.media.length > 0">
- <summary>({{ '%i18n:@media-count%'.replace('{}', note.media.length) }})</summary>
- <mk-media-list :media-list="note.media"/>
+ <details v-if="note.files.length > 0">
+ <summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary>
+ <mk-media-list :media-list="note.files"/>
</details>
<details v-if="note.poll">
<summary>%i18n:@poll%</summary>
diff --git a/src/client/app/desktop/views/components/taskmanager.vue b/src/client/app/desktop/views/components/taskmanager.vue
deleted file mode 100644
index 1f1385add8..0000000000
--- a/src/client/app/desktop/views/components/taskmanager.vue
+++ /dev/null
@@ -1,219 +0,0 @@
-<template>
-<mk-window ref="window" width="750px" height="500px" @closed="$destroy" name="TaskManager">
- <span slot="header" :class="$style.header">%fa:stethoscope%%i18n:@title%</span>
- <el-tabs :class="$style.content">
- <el-tab-pane label="Requests">
- <el-table
- :data="os.requests"
- style="width: 100%"
- :default-sort="{prop: 'date', order: 'descending'}"
- >
- <el-table-column type="expand">
- <template slot-scope="props">
- <pre>{{ props.row.data }}</pre>
- <pre>{{ props.row.res }}</pre>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Requested at"
- prop="date"
- sortable
- >
- <template slot-scope="scope">
- <b style="margin-right: 8px">{{ scope.row.date.getTime() }}</b>
- <span>(<mk-time :time="scope.row.date"/>)</span>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Name"
- >
- <template slot-scope="scope">
- <b>{{ scope.row.name }}</b>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Status"
- >
- <template slot-scope="scope">
- <span>{{ scope.row.status || '(pending)' }}</span>
- </template>
- </el-table-column>
- </el-table>
- </el-tab-pane>
-
- <el-tab-pane label="Streams">
- <el-table
- :data="os.connections"
- style="width: 100%"
- >
- <el-table-column
- label="Uptime"
- >
- <template slot-scope="scope">
- <mk-timer v-if="scope.row.connectedAt" :time="scope.row.connectedAt"/>
- <span v-else>-</span>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Name"
- >
- <template slot-scope="scope">
- <b>{{ scope.row.name == '' ? '[Home]' : scope.row.name }}</b>
- </template>
- </el-table-column>
-
- <el-table-column
- label="User"
- >
- <template slot-scope="scope">
- <span>{{ scope.row.user || '(anonymous)' }}</span>
- </template>
- </el-table-column>
-
- <el-table-column
- prop="state"
- label="State"
- />
-
- <el-table-column
- prop="in"
- label="In"
- />
-
- <el-table-column
- prop="out"
- label="Out"
- />
- </el-table>
- </el-tab-pane>
-
- <el-tab-pane label="Streams (Inspect)">
- <el-tabs type="card" style="height:50%">
- <el-tab-pane v-for="c in os.connections" :label="c.name == '' ? '[Home]' : c.name" :key="c.id" :name="c.id" ref="connectionsTab">
- <div style="padding: 12px 0 0 12px">
- <el-button size="mini" @click="send(c)">Send</el-button>
- <el-button size="mini" type="warning" @click="c.isSuspended = true" v-if="!c.isSuspended">Suspend</el-button>
- <el-button size="mini" type="success" @click="c.isSuspended = false" v-else>Resume</el-button>
- <el-button size="mini" type="danger" @click="c.close">Disconnect</el-button>
- </div>
-
- <el-table
- :data="c.inout"
- style="width: 100%"
- :default-sort="{prop: 'at', order: 'descending'}"
- >
- <el-table-column type="expand">
- <template slot-scope="props">
- <pre>{{ props.row.data }}</pre>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Date"
- prop="at"
- sortable
- >
- <template slot-scope="scope">
- <b style="margin-right: 8px">{{ scope.row.at.getTime() }}</b>
- <span>(<mk-time :time="scope.row.at"/>)</span>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Type"
- >
- <template slot-scope="scope">
- <span>{{ getMessageType(scope.row.data) }}</span>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Incoming / Outgoing"
- prop="type"
- />
- </el-table>
- </el-tab-pane>
- </el-tabs>
- </el-tab-pane>
-
- <el-tab-pane label="Windows">
- <el-table
- :data="Array.from(os.windows.windows)"
- style="width: 100%"
- >
- <el-table-column
- label="Name"
- >
- <template slot-scope="scope">
- <b>{{ scope.row.name || '(unknown)' }}</b>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Operations"
- >
- <template slot-scope="scope">
- <el-button size="mini" type="danger" @click="scope.row.close">Close</el-button>
- </template>
- </el-table-column>
- </el-table>
- </el-tab-pane>
- </el-tabs>
-</mk-window>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
- mounted() {
- (this as any).os.windows.on('added', this.onWindowsChanged);
- (this as any).os.windows.on('removed', this.onWindowsChanged);
- },
- beforeDestroy() {
- (this as any).os.windows.off('added', this.onWindowsChanged);
- (this as any).os.windows.off('removed', this.onWindowsChanged);
- },
- methods: {
- getMessageType(data): string {
- return data.type ? data.type : '-';
- },
- onWindowsChanged() {
- this.$forceUpdate();
- },
- send(c) {
- (this as any).apis.input({
- title: 'Send a JSON message',
- allowEmpty: false
- }).then(json => {
- c.send(JSON.parse(json));
- });
- }
- }
-});
-</script>
-
-<style lang="stylus" module>
-.header
- > [data-fa]
- margin-right 4px
-
-.content
- height 100%
- overflow auto
-
-</style>
-
-<style>
-.el-tabs__header {
- margin-bottom: 0 !important;
-}
-
-.el-tabs__item {
- padding: 0 20px !important;
-}
-</style>
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue
index 25fd5d36ac..d2176dee87 100644
--- a/src/client/app/desktop/views/components/timeline.core.vue
+++ b/src/client/app/desktop/views/components/timeline.core.vue
@@ -15,6 +15,7 @@
<script lang="ts">
import Vue from 'vue';
+import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
const fetchLimit = 10;
@@ -23,6 +24,9 @@ export default Vue.extend({
src: {
type: String,
required: true
+ },
+ tagTl: {
+ required: false
}
},
@@ -31,6 +35,7 @@ export default Vue.extend({
fetching: true,
moreFetching: false,
existMore: false,
+ streamManager: null,
connection: null,
connectionId: null,
date: null
@@ -42,21 +47,14 @@ export default Vue.extend({
return this.$store.state.i.followingCount == 0;
},
- stream(): any {
- switch (this.src) {
- case 'home': return (this as any).os.stream;
- case 'local': return (this as any).os.streams.localTimelineStream;
- case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
- case 'global': return (this as any).os.streams.globalTimelineStream;
- }
- },
-
endpoint(): string {
switch (this.src) {
case 'home': return 'notes/timeline';
case 'local': return 'notes/local-timeline';
case 'hybrid': return 'notes/hybrid-timeline';
case 'global': return 'notes/global-timeline';
+ case 'mentions': return 'notes/mentions';
+ case 'tag': return 'notes/search_by_tag';
}
},
@@ -66,13 +64,36 @@ export default Vue.extend({
},
mounted() {
- this.connection = this.stream.getConnection();
- this.connectionId = this.stream.use();
-
- this.connection.on('note', this.onNote);
- if (this.src == 'home') {
+ if (this.src == 'tag') {
+ this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
+ this.connection.on('note', this.onNote);
+ } else if (this.src == 'home') {
+ this.streamManager = (this as any).os.stream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', this.onNote);
this.connection.on('follow', this.onChangeFollowing);
this.connection.on('unfollow', this.onChangeFollowing);
+ } else if (this.src == 'local') {
+ this.streamManager = (this as any).os.streams.localTimelineStream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', this.onNote);
+ } else if (this.src == 'hybrid') {
+ this.streamManager = (this as any).os.streams.hybridTimelineStream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', this.onNote);
+ } else if (this.src == 'global') {
+ this.streamManager = (this as any).os.streams.globalTimelineStream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', this.onNote);
+ } else if (this.src == 'mentions') {
+ this.streamManager = (this as any).os.stream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('mention', this.onNote);
}
document.addEventListener('keydown', this.onKeydown);
@@ -81,12 +102,27 @@ export default Vue.extend({
},
beforeDestroy() {
- this.connection.off('note', this.onNote);
- if (this.src == 'home') {
+ if (this.src == 'tag') {
+ this.connection.off('note', this.onNote);
+ this.connection.close();
+ } else if (this.src == 'home') {
+ this.connection.off('note', this.onNote);
this.connection.off('follow', this.onChangeFollowing);
this.connection.off('unfollow', this.onChangeFollowing);
+ this.streamManager.dispose(this.connectionId);
+ } else if (this.src == 'local') {
+ this.connection.off('note', this.onNote);
+ this.streamManager.dispose(this.connectionId);
+ } else if (this.src == 'hybrid') {
+ this.connection.off('note', this.onNote);
+ this.streamManager.dispose(this.connectionId);
+ } else if (this.src == 'global') {
+ this.connection.off('note', this.onNote);
+ this.streamManager.dispose(this.connectionId);
+ } else if (this.src == 'mentions') {
+ this.connection.off('mention', this.onNote);
+ this.streamManager.dispose(this.connectionId);
}
- this.stream.dispose(this.connectionId);
document.removeEventListener('keydown', this.onKeydown);
},
@@ -101,7 +137,8 @@ export default Vue.extend({
untilDate: this.date ? this.date.getTime() : undefined,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
+ query: this.tagTl ? this.tagTl.query : undefined
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
@@ -124,7 +161,8 @@ export default Vue.extend({
untilId: (this.$refs.timeline as any).tail().id,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
+ query: this.tagTl ? this.tagTl.query : undefined
});
promise.then(notes => {
diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index 52a7753438..2dc84004df 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -2,16 +2,23 @@
<div class="mk-timeline">
<header>
<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 == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span>
+ <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
+ <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
+ <span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span>
+ <span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl">%fa:hashtag% {{ tagTl.title }}</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>
+ <div class="buttons">
+ <button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button>
+ <button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button>
+ </div>
</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 == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/>
+ <x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
+ <x-core v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/>
<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
</div>
</template>
@@ -19,7 +26,8 @@
<script lang="ts">
import Vue from 'vue';
import XCore from './timeline.core.vue';
-import MkUserListsWindow from './user-lists-window.vue';
+import Menu from '../../../common/views/components/menu.vue';
+import MkSettingsWindow from './settings-window.vue';
export default Vue.extend({
components: {
@@ -29,7 +37,9 @@ export default Vue.extend({
data() {
return {
src: 'home',
- list: null
+ list: null,
+ tagTl: null,
+ enableLocalTimeline: false
};
},
@@ -38,16 +48,28 @@ export default Vue.extend({
this.saveSrc();
},
- list() {
+ list(x) {
this.saveSrc();
+ if (x != null) this.tagTl = null;
+ },
+
+ tagTl(x) {
+ this.saveSrc();
+ if (x != null) this.list = null;
}
},
created() {
+ (this as any).os.getMeta().then(meta => {
+ this.enableLocalTimeline = !meta.disableLocalTimeline;
+ });
+
if (this.$store.state.device.tl) {
this.src = this.$store.state.device.tl.src;
if (this.src == 'list') {
this.list = this.$store.state.device.tl.arg;
+ } else if (this.src == 'tag') {
+ this.tagTl = this.$store.state.device.tl.arg;
}
} else if (this.$store.state.i.followingCount == 0) {
this.src = 'hybrid';
@@ -64,7 +86,7 @@ export default Vue.extend({
saveSrc() {
this.$store.commit('device/setTl', {
src: this.src,
- arg: this.list
+ arg: this.src == 'list' ? this.list : this.tagTl
});
},
@@ -72,12 +94,74 @@ export default Vue.extend({
(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();
+ async chooseList() {
+ const lists = await (this as any).api('users/lists/list');
+
+ let menu = [{
+ icon: '%fa:plus%',
+ text: '%i18n:@add-list%',
+ action: () => {
+ (this as any).apis.input({
+ title: '%i18n:@list-name%',
+ }).then(async title => {
+ const list = await (this as any).api('users/lists/create', {
+ title
+ });
+
+ this.list = list;
+ this.src = 'list';
+ });
+ }
+ }];
+
+ if (lists.length > 0) {
+ menu.push(null);
+ }
+
+ menu = menu.concat(lists.map(list => ({
+ icon: '%fa:list%',
+ text: list.title,
+ action: () => {
+ this.list = list;
+ this.src = 'list';
+ }
+ })));
+
+ this.os.new(Menu, {
+ source: this.$refs.listButton,
+ compact: false,
+ items: menu
+ });
+ },
+
+ chooseTag() {
+ let menu = [{
+ icon: '%fa:plus%',
+ text: '%i18n:@add-tag-timeline%',
+ action: () => {
+ (this as any).os.new(MkSettingsWindow, {
+ initialPage: 'hashtags'
+ });
+ }
+ }];
+
+ if (this.$store.state.settings.tagTimelines.length > 0) {
+ menu.push(null);
+ }
+
+ menu = menu.concat(this.$store.state.settings.tagTimelines.map(t => ({
+ icon: '%fa:hashtag%',
+ text: t.title,
+ action: () => {
+ this.tagTl = t;
+ this.src = 'tag';
+ }
+ })));
+
+ this.os.new(Menu, {
+ source: this.$refs.tagButton,
+ compact: false,
+ items: menu
});
}
}
@@ -99,22 +183,24 @@ root(isDark)
border-radius 6px 6px 0 0
box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
- > button
+ > .buttons
position absolute
z-index 2
top 0
right 0
- padding 0
- width 42px
- font-size 0.9em
- line-height 42px
- color isDark ? #9baec8 : #ccc
+ padding-right 8px
- &:hover
- color isDark ? #b2c1d5 : #aaa
+ > button
+ padding 0 8px
+ font-size 0.9em
+ line-height 42px
+ color isDark ? #9baec8 : #ccc
+
+ &:hover
+ color isDark ? #b2c1d5 : #aaa
- &:active
- color isDark ? #b2c1d5 : #999
+ &:active
+ color isDark ? #b2c1d5 : #999
> span
display inline-block
diff --git a/src/client/app/desktop/views/components/ui-notification.vue b/src/client/app/desktop/views/components/ui-notification.vue
index 68413914c0..7519124870 100644
--- a/src/client/app/desktop/views/components/ui-notification.vue
+++ b/src/client/app/desktop/views/components/ui-notification.vue
@@ -27,7 +27,7 @@ export default Vue.extend({
translateY: -64,
duration: 500,
easing: 'easeInElastic',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}, 6000);
});
diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
index 6de4eaf744..ac8a6c7765 100644
--- a/src/client/app/desktop/views/components/ui.header.vue
+++ b/src/client/app/desktop/views/components/ui.header.vue
@@ -1,5 +1,6 @@
<template>
<div class="header">
+ <p class="warn" v-if="env != 'production'">%i18n:common.do-not-use-in-production%</p>
<mk-special-message/>
<div class="main" ref="main">
<div class="backdrop"></div>
@@ -28,6 +29,7 @@
<script lang="ts">
import Vue from 'vue';
import * as anime from 'animejs';
+import { env } from '../../../config';
import XNav from './ui.header.nav.vue';
import XSearch from './ui.header.search.vue';
@@ -43,7 +45,13 @@ export default Vue.extend({
XAccount,
XNotifications,
XPost,
- XClock,
+ XClock
+ },
+
+ data() {
+ return {
+ env: env
+ };
},
mounted() {
@@ -119,6 +127,15 @@ root(isDark)
width 100%
box-shadow 0 1px 1px rgba(#000, 0.075)
+ > .warn
+ display block
+ margin 0
+ padding 4px
+ text-align center
+ font-size 12px
+ background #f00
+ color #fff
+
> .main
height 48px
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index 1e1755ec3c..f6d6d68a7f 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -75,7 +75,7 @@ export default Vue.extend({
'margin-top': '-8px',
duration: 200,
easing: 'easeOutQuad',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}
}
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 262fd38cd1..f42d577fce 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -1,17 +1,16 @@
<template>
-<div class="root item">
- <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>
- <span class="username">@{{ user | acct }}</span>
- </header>
- <div class="body">
- <p class="followed" v-if="user.isFollowed">%i18n:@followed%</p>
- <div class="description">{{ user.description }}</div>
+<div class="zvdbznxvfixtmujpsigoccczftvpiwqh">
+ <div class="banner" :style="bannerStyle"></div>
+ <mk-avatar class="avatar" :user="user" :disable-preview="true"/>
+ <div class="body">
+ <router-link :to="user | userPage" class="name">{{ user | userName }}</router-link>
+ <span class="username">@{{ user | acct }}</span>
+ <div class="description">
+ <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
</div>
+ <p class="followed" v-if="user.isFollowed">%i18n:@followed%</p>
+ <mk-follow-button :user="user" :size="'big'"/>
</div>
- <mk-follow-button :user="user"/>
</div>
</template>
@@ -19,76 +18,69 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['user']
+ props: ['user'],
+
+ computed: {
+ bannerStyle(): any {
+ if (this.user.bannerUrl == null) return {};
+ return {
+ backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
+ backgroundImage: `url(${ this.user.bannerUrl })`
+ };
+ }
+ },
});
</script>
<style lang="stylus" scoped>
-.root.item
- padding 16px
+.zvdbznxvfixtmujpsigoccczftvpiwqh
+ $bg = #fff
+
+ margin 16px auto
+ max-width calc(100% - 32px)
font-size 16px
+ text-align center
+ background $bg
+ box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
- &:after
- content ""
- display block
- clear both
+ > .banner
+ height 100px
+ background-color #f9f4f4
+ background-position center
+ background-size cover
> .avatar
display block
- float left
- margin 0 16px 0 0
- width 58px
- height 58px
- border-radius 8px
-
- > .main
- float left
- width calc(100% - 74px)
-
- > header
- margin-bottom 2px
+ margin -40px auto 0 auto
+ width 80px
+ height 80px
+ border-radius 100%
+ border solid 4px $bg
- > .name
- display inline
- margin 0
- padding 0
- color #777
- font-size 1em
- font-weight 700
- text-align left
- text-decoration none
+ > .body
+ padding 4px 32px 32px 32px
- &:hover
- text-decoration underline
+ @media (max-width 400px)
+ padding 4px 16px 16px 16px
- > .username
- text-align left
- margin 0 0 0 8px
- color #ccc
+ > .name
+ font-size 20px
+ font-weight bold
- > .body
- > .followed
- display inline-block
- margin 0 0 4px 0
- padding 2px 8px
- vertical-align top
- font-size 10px
- color #71afc7
- background #eefaff
- border-radius 4px
+ > .username
+ display block
+ opacity 0.7
- > .description
- cursor default
- display block
- margin 0
- padding 0
- overflow-wrap break-word
- font-size 1.1em
- color #717171
+ > .description
+ margin 16px 0
- > .mk-follow-button
- position absolute
- top 16px
- right 16px
+ > .followed
+ margin 0 0 16px 0
+ padding 0
+ line-height 24px
+ font-size 0.8em
+ color #71afc7
+ background #eefaff
+ border-radius 4px
</style>
diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue
index 0423db8ed7..05e2f4e5b3 100644
--- a/src/client/app/desktop/views/components/users-list.vue
+++ b/src/client/app/desktop/views/components/users-list.vue
@@ -33,7 +33,7 @@ export default Vue.extend({
props: ['fetch', 'count', 'youKnowCount'],
data() {
return {
- limit: 30,
+ limit: 20,
mode: 'all',
fetching: true,
moreFetching: false,
@@ -73,10 +73,14 @@ export default Vue.extend({
.mk-users-list
height 100%
- background #fff
+ overflow auto
+ background #eee
> nav
- z-index 1
+ z-index 10
+ position sticky
+ top 0
+ background #fff
box-shadow 0 1px 0 rgba(#000, 0.1)
> div
@@ -114,16 +118,14 @@ export default Vue.extend({
background #eee
border-radius 20px
- > .users
- height calc(100% - 54px)
- overflow auto
-
- > *
- border-bottom solid 1px rgba(#000, 0.05)
+ > button
+ display block
+ width calc(100% - 32px)
+ margin 16px
+ padding 16px
- > *
- max-width 600px
- margin 0 auto
+ &:hover
+ background rgba(#000, 0.1)
> .no
margin 0
diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
index ec044ad27e..30f0ec558f 100644
--- a/src/client/app/desktop/views/components/window.vue
+++ b/src/client/app/desktop/views/components/window.vue
@@ -106,7 +106,7 @@ export default Vue.extend({
mounted() {
if (this.preventMount) {
- this.$destroy();
+ this.destroyDom();
return;
}
@@ -190,7 +190,7 @@ export default Vue.extend({
});
setTimeout(() => {
- this.$destroy();
+ this.destroyDom();
this.$emit('closed');
}, 300);
},
diff --git a/src/client/app/desktop/views/pages/admin/admin.announcements.vue b/src/client/app/desktop/views/pages/admin/admin.announcements.vue
new file mode 100644
index 0000000000..532400deb2
--- /dev/null
+++ b/src/client/app/desktop/views/pages/admin/admin.announcements.vue
@@ -0,0 +1,41 @@
+<template>
+<div class="qldxjjsrseehkusjuoooapmsprvfrxyl mk-admin-card">
+ <header>%i18n:@announcements%</header>
+ <textarea v-model="broadcasts"></textarea>
+ <button class="ui" @click="save">%i18n:@save%</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+
+export default Vue.extend({
+ data() {
+ return {
+ broadcasts: '',
+ };
+ },
+ created() {
+ (this as any).os.getMeta().then(meta => {
+ this.broadcasts = JSON.stringify(meta.broadcasts, null, ' ');
+ });
+ },
+ methods: {
+ save() {
+ (this as any).api('admin/update-meta', {
+ broadcasts: JSON.parse(this.broadcasts)
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.qldxjjsrseehkusjuoooapmsprvfrxyl
+ textarea
+ width 100%
+ min-height 300px
+
+</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
index ebb54d782e..c86c30db17 100644
--- a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
@@ -1,22 +1,34 @@
<template>
<div class="obdskegsannmntldydackcpzezagxqfy mk-admin-card">
<header>%i18n:@dashboard%</header>
+
<div v-if="stats" class="stats">
<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div>
<div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
</div>
+
<div class="cpu-memory">
<x-cpu-memory :connection="connection"/>
</div>
- <div>
- <label>
- <input type="checkbox" v-model="disableRegistration" @change="updateMeta">
- <span>disableRegistration</span>
- </label>
- <button class="ui" @click="invite">%i18n:@invite%</button>
- <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
+
+ <div class="form">
+ <div>
+ <label>
+ <input type="checkbox" v-model="disableRegistration" @change="updateMeta">
+ <span>%i18n:@disableRegistration%</span>
+ </label>
+ <button class="ui" @click="invite">%i18n:@invite%</button>
+ <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
+ </div>
+
+ <div>
+ <label>
+ <input type="checkbox" v-model="disableLocalTimeline" @change="updateMeta">
+ <span>%i18n:@disableLocalTimeline%</span>
+ </label>
+ </div>
</div>
</div>
</template>
@@ -33,6 +45,7 @@ export default Vue.extend({
return {
stats: null,
disableRegistration: false,
+ disableLocalTimeline: false,
inviteCode: null,
connection: null,
connectionId: null
@@ -44,6 +57,7 @@ export default Vue.extend({
(this as any).os.getMeta().then(meta => {
this.disableRegistration = meta.disableRegistration;
+ this.disableLocalTimeline = meta.disableLocalTimeline;
});
(this as any).api('stats').then(stats => {
@@ -61,7 +75,8 @@ export default Vue.extend({
},
updateMeta() {
(this as any).api('admin/update-meta', {
- disableRegistration: this.disableRegistration
+ disableRegistration: this.disableRegistration,
+ disableLocalTimeline: this.disableLocalTimeline
});
}
}
@@ -97,4 +112,8 @@ export default Vue.extend({
border solid 1px #eee
border-radius: 8px
+ > .form
+ > div
+ border-bottom solid 1px #eee
+
</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.hashtags.vue b/src/client/app/desktop/views/pages/admin/admin.hashtags.vue
new file mode 100644
index 0000000000..c6bf20361f
--- /dev/null
+++ b/src/client/app/desktop/views/pages/admin/admin.hashtags.vue
@@ -0,0 +1,41 @@
+<template>
+<div class="jdnqwkzlnxcfftthoybjxrebyolvoucw mk-admin-card">
+ <header>%i18n:@hided-tags%</header>
+ <textarea v-model="hidedTags"></textarea>
+ <button class="ui" @click="save">%i18n:@save%</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+
+export default Vue.extend({
+ data() {
+ return {
+ hidedTags: '',
+ };
+ },
+ created() {
+ (this as any).os.getMeta().then(meta => {
+ this.hidedTags = meta.hidedTags.join('\n');
+ });
+ },
+ methods: {
+ save() {
+ (this as any).api('admin/update-meta', {
+ hidedTags: this.hidedTags.split('\n')
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.jdnqwkzlnxcfftthoybjxrebyolvoucw
+ textarea
+ width 100%
+ min-height 300px
+
+</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.vue b/src/client/app/desktop/views/pages/admin/admin.vue
index 3438462cd6..510252b447 100644
--- a/src/client/app/desktop/views/pages/admin/admin.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.vue
@@ -4,6 +4,9 @@
<ul>
<li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }">%fa:chalkboard .fw%%i18n:@dashboard%</li>
<li @click="nav('users')" :class="{ active: page == 'users' }">%fa:users .fw%%i18n:@users%</li>
+ <li @click="nav('announcements')" :class="{ active: page == 'announcements' }">%fa:broadcast-tower .fw%%i18n:@announcements%</li>
+ <li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }">%fa:hashtag .fw%%i18n:@hashtags%</li>
+
<!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }">%fa:cloud .fw%%i18n:@drive%</li> -->
<!-- <li @click="nav('update')" :class="{ active: page == 'update' }">%i18n:@update%</li> -->
</ul>
@@ -13,6 +16,12 @@
<x-dashboard/>
<x-charts/>
</div>
+ <div v-show="page == 'announcements'">
+ <x-announcements/>
+ </div>
+ <div v-show="page == 'hashtags'">
+ <x-hashtags/>
+ </div>
<div v-if="page == 'users'">
<x-suspend-user/>
<x-unsuspend-user/>
@@ -28,6 +37,8 @@
<script lang="ts">
import Vue from "vue";
import XDashboard from "./admin.dashboard.vue";
+import XAnnouncements from "./admin.announcements.vue";
+import XHashtags from "./admin.hashtags.vue";
import XSuspendUser from "./admin.suspend-user.vue";
import XUnsuspendUser from "./admin.unsuspend-user.vue";
import XVerifyUser from "./admin.verify-user.vue";
@@ -37,6 +48,8 @@ import XCharts from "../../components/charts.vue";
export default Vue.extend({
components: {
XDashboard,
+ XAnnouncements,
+ XHashtags,
XSuspendUser,
XUnsuspendUser,
XVerifyUser,
diff --git a/src/client/app/desktop/views/pages/deck/deck.column-core.vue b/src/client/app/desktop/views/pages/deck/deck.column-core.vue
index 7f219c0be1..a320f697b3 100644
--- a/src/client/app/desktop/views/pages/deck/deck.column-core.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.column-core.vue
@@ -6,6 +6,8 @@
<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/>
+<x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked"/>
+<x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked"/>
</template>
<script lang="ts">
@@ -13,12 +15,14 @@ import Vue from 'vue';
import XTlColumn from './deck.tl-column.vue';
import XNotificationsColumn from './deck.notifications-column.vue';
import XWidgetsColumn from './deck.widgets-column.vue';
+import XMentionsColumn from './deck.mentions-column.vue';
export default Vue.extend({
components: {
XTlColumn,
XNotificationsColumn,
- XWidgetsColumn
+ XWidgetsColumn,
+ XMentionsColumn
},
props: {
diff --git a/src/client/app/desktop/views/pages/deck/deck.column.vue b/src/client/app/desktop/views/pages/deck/deck.column.vue
index d59d430da6..abb09775fb 100644
--- a/src/client/app/desktop/views/pages/deck/deck.column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.column.vue
@@ -3,18 +3,20 @@
@dragover.prevent.stop="onDragover"
@dragenter.prevent="onDragenter"
@dragleave="onDragleave"
- @drop.prevent.stop="onDrop"
->
+ @drop.prevent.stop="onDrop">
<header :class="{ indicate: count > 0 }"
draggable="true"
- @click="toggleActive"
+ @click="goTop"
@dragstart="onDragstart"
@dragend="onDragend"
- @contextmenu.prevent.stop="onContextmenu"
- >
+ @contextmenu.prevent.stop="onContextmenu">
+ <button class="toggleActive" @click="toggleActive" v-if="isStacked">
+ <template v-if="active">%fa:angle-up%</template>
+ <template v-else>%fa:angle-down%</template>
+ </button>
<slot name="header"></slot>
<span class="count" v-if="count > 0">({{ count }})</span>
- <button ref="menu" @click.stop="showMenu">%fa:caret-down%</button>
+ <button class="menu" ref="menu" @click.stop="showMenu">%fa:caret-down%</button>
</header>
<div ref="body" v-show="active">
<slot></slot>
@@ -26,6 +28,7 @@
import Vue from 'vue';
import Menu from '../../../../common/views/components/menu.vue';
import contextmenu from '../../../api/contextmenu';
+import { countIf } from '../../../../../../prelude/array';
export default Vue.extend({
props: {
@@ -115,7 +118,7 @@ export default Vue.extend({
toggleActive() {
if (!this.isStacked) return;
const vms = this.$store.state.settings.deck.layout.find(ids => ids.indexOf(this.column.id) != -1).map(id => this.getColumnVm(id));
- if (this.active && vms.filter(vm => vm.$el.classList.contains('active')).length == 1) return;
+ if (this.active && countIf(vm => vm.$el.classList.contains('active'), vms) == 1) return;
this.active = !this.active;
},
@@ -211,6 +214,13 @@ export default Vue.extend({
});
},
+ goTop() {
+ this.$refs.body.scrollTo({
+ top: 0,
+ behavior: 'smooth'
+ });
+ },
+
onDragstart(e) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('mk-deck-column', this.column.id);
@@ -302,6 +312,7 @@ root(isDark)
color #bbb
> header
+ display flex
z-index 1
line-height $header-height
padding 0 16px
@@ -328,10 +339,8 @@ root(isDark)
margin-left 4px
opacity 0.5
- > button
- position absolute
- top 0
- right 0
+ > .toggleActive
+ > .menu
width $header-height
line-height $header-height
font-size 16px
@@ -343,6 +352,13 @@ root(isDark)
&:active
color isDark ? #b2c1d5 : #999
+ > .toggleActive
+ margin-left -16px
+
+ > .menu
+ margin-left auto
+ margin-right -16px
+
> div
height "calc(100% - %s)" % $header-height
overflow auto
diff --git a/src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue b/src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
new file mode 100644
index 0000000000..f38d5a6df5
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
@@ -0,0 +1,117 @@
+<template>
+ <x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XNotes from './deck.notes.vue';
+import { HashtagStream } from '../../../../common/scripts/streaming/hashtag';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ components: {
+ XNotes
+ },
+
+ props: {
+ tagTl: {
+ type: Object,
+ required: true
+ },
+ mediaOnly: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ mediaView: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
+
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null
+ };
+ },
+
+ watch: {
+ mediaOnly() {
+ this.fetch();
+ }
+ },
+
+ mounted() {
+ if (this.connection) this.connection.close();
+ this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
+ this.connection.on('note', this.onNote);
+
+ this.fetch();
+ },
+
+ beforeDestroy() {
+ this.connection.close();
+ },
+
+ methods: {
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('notes/search_by_tag', {
+ limit: fetchLimit + 1,
+ withFiles: this.mediaOnly,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
+ query: this.tagTl.query
+ }).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;
+
+ const promise = (this as any).api('notes/search_by_tag', {
+ limit: fetchLimit + 1,
+ untilId: (this.$refs.timeline as any).tail().id,
+ withFiles: this.mediaOnly,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
+ query: this.tagTl.query
+ });
+
+ promise.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;
+ });
+
+ return promise;
+ },
+ onNote(note) {
+ if (this.mediaOnly && note.files.length == 0) return;
+
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
+ }
+ }
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.list-tl.vue b/src/client/app/desktop/views/pages/deck/deck.list-tl.vue
index 70048f99e3..e82e76e4d0 100644
--- a/src/client/app/desktop/views/pages/deck/deck.list-tl.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.list-tl.vue
@@ -68,7 +68,7 @@ export default Vue.extend({
(this as any).api('notes/user-list-timeline', {
listId: this.list.id,
limit: fetchLimit + 1,
- mediaOnly: this.mediaOnly,
+ withFiles: this.mediaOnly,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
@@ -90,7 +90,7 @@ export default Vue.extend({
listId: this.list.id,
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id,
- mediaOnly: this.mediaOnly,
+ withFiles: this.mediaOnly,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
@@ -109,7 +109,7 @@ export default Vue.extend({
return promise;
},
onNote(note) {
- if (this.mediaOnly && note.media.length == 0) return;
+ if (this.mediaOnly && note.files.length == 0) return;
// Prepend a note
(this.$refs.timeline as any).prepend(note);
diff --git a/src/client/app/desktop/views/pages/deck/deck.mentions-column.vue b/src/client/app/desktop/views/pages/deck/deck.mentions-column.vue
new file mode 100644
index 0000000000..8ec10164f2
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.mentions-column.vue
@@ -0,0 +1,38 @@
+<template>
+<x-column :name="name" :column="column" :is-stacked="isStacked">
+ <span slot="header">%fa:at%{{ name }}</span>
+
+ <x-mentions/>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XColumn from './deck.column.vue';
+import XMentions from './deck.mentions.vue';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XMentions
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ computed: {
+ name(): string {
+ if (this.column.name) return this.column.name;
+ return '%i18n:common.deck.mentions%';
+ }
+ },
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.mentions.vue b/src/client/app/desktop/views/pages/deck/deck.mentions.vue
new file mode 100644
index 0000000000..cecb75f067
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.mentions.vue
@@ -0,0 +1,93 @@
+<template>
+ <x-notes ref="timeline" :more="existMore ? more : null"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XNotes from './deck.notes.vue';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ components: {
+ XNotes
+ },
+
+ props: {
+ },
+
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null,
+ connectionId: null
+ };
+ },
+
+ mounted() {
+ this.connection = (this as any).os.stream.getConnection();
+ this.connectionId = (this as any).os.stream.use();
+
+ this.connection.on('mention', this.onNote);
+
+ this.fetch();
+ },
+
+ beforeDestroy() {
+ this.connection.off('mention', this.onNote);
+ (this as any).os.stream.dispose(this.connectionId);
+ },
+
+ methods: {
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('notes/mentions', {
+ limit: fetchLimit + 1,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ }).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;
+
+ const promise = (this as any).api('notes/mentions', {
+ limit: fetchLimit + 1,
+ untilId: (this.$refs.timeline as any).tail().id,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ });
+
+ promise.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;
+ });
+
+ return promise;
+ },
+ onNote(note) {
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
+ }
+ }
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.note.vue b/src/client/app/desktop/views/pages/deck/deck.note.vue
index e6d062eac9..980fb03136 100644
--- a/src/client/app/desktop/views/pages/deck/deck.note.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.note.vue
@@ -18,7 +18,7 @@
<div class="body">
<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 ? '%i18n:@less%' : '%i18n:@more%' }}</span>
+ <mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
@@ -28,14 +28,15 @@
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
<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 class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote" :mini="true"/>
</div>
+ <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="false" :mini="true"/>
</div>
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
</div>
@@ -53,11 +54,11 @@
</article>
</div>
<div v-else class="srwrkujossgfuhrbnvqkybtzxpblgchi">
- <div v-if="note.media.length > 0">
- <mk-media-list :media-list="note.media"/>
+ <div v-if="note.files.length > 0">
+ <mk-media-list :media-list="note.files"/>
</div>
- <div v-if="note.renote && note.renote.media.length > 0">
- <mk-media-list :media-list="note.renote.media"/>
+ <div v-if="note.renote && note.renote.files.length > 0">
+ <mk-media-list :media-list="note.renote.files"/>
</div>
</div>
</template>
@@ -99,7 +100,7 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.length == 0 &&
this.note.poll == null);
},
@@ -370,7 +371,7 @@ root(isDark)
.mk-url-preview
margin-top 8px
- > .media
+ > .files
> img
display block
max-width 100%
@@ -393,7 +394,7 @@ root(isDark)
> .renote
margin 8px 0
- > .mk-note-preview
+ > *
padding 16px
border dashed 1px isDark ? #4e945e : #c0dac6
border-radius 8px
diff --git a/src/client/app/desktop/views/pages/deck/deck.notes.vue b/src/client/app/desktop/views/pages/deck/deck.notes.vue
index f7fca5de92..2e7e30f12a 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notes.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notes.vue
@@ -127,7 +127,7 @@ export default Vue.extend({
prepend(note, silent = false) {
//#region 弾く
const isMyNote = note.userId == this.$store.state.i.id;
- const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
+ const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
if (this.$store.state.settings.showMyRenotes === false) {
if (isMyNote && isPureRenote) {
diff --git a/src/client/app/desktop/views/pages/deck/deck.notifications.vue b/src/client/app/desktop/views/pages/deck/deck.notifications.vue
index fcb74b9140..f73f221b7b 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notifications.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notifications.vue
@@ -1,8 +1,7 @@
<template>
<div class="oxynyeqmfvracxnglgulyqfgqxnxmehl">
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!--<transition-group name="mk-notifications" class="transition notifications">-->
- <div class="notifications">
+ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications">
<template v-for="(notification, i) in _notifications">
<x-notification class="notification" :notification="notification" :key="notification.id"/>
<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
@@ -10,8 +9,7 @@
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!--</transition-group>-->
+ </component>
<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%' }}
</button>
diff --git a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue
index 231b505f5d..550b1be628 100644
--- a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue
@@ -6,6 +6,7 @@
<template v-if="column.type == 'hybrid'">%fa:share-alt%</template>
<template v-if="column.type == 'global'">%fa:globe%</template>
<template v-if="column.type == 'list'">%fa:list%</template>
+ <template v-if="column.type == 'hashtag'">%fa:hashtag%</template>
<span>{{ name }}</span>
</span>
@@ -14,6 +15,7 @@
<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/>
</div>
<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
+ <x-hashtag-tl v-if="column.type == 'hashtag'" :tag-tl="$store.state.settings.tagTimelines.find(x => x.id == column.tagTlId)" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
<x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
</x-column>
</template>
@@ -23,12 +25,14 @@ import Vue from 'vue';
import XColumn from './deck.column.vue';
import XTl from './deck.tl.vue';
import XListTl from './deck.list-tl.vue';
+import XHashtagTl from './deck.hashtag-tl.vue';
export default Vue.extend({
components: {
XColumn,
XTl,
- XListTl
+ XListTl,
+ XHashtagTl
},
props: {
@@ -65,6 +69,7 @@ export default Vue.extend({
case 'hybrid': return '%i18n:common.deck.hybrid%';
case 'global': return '%i18n:common.deck.global%';
case 'list': return this.column.list.title;
+ case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title;
}
}
},
diff --git a/src/client/app/desktop/views/pages/deck/deck.tl.vue b/src/client/app/desktop/views/pages/deck/deck.tl.vue
index a9e4d489c3..120ceb7fc2 100644
--- a/src/client/app/desktop/views/pages/deck/deck.tl.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.tl.vue
@@ -96,7 +96,7 @@ export default Vue.extend({
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api(this.endpoint, {
limit: fetchLimit + 1,
- mediaOnly: this.mediaOnly,
+ withFiles: this.mediaOnly,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
@@ -117,7 +117,7 @@ export default Vue.extend({
const promise = (this as any).api(this.endpoint, {
limit: fetchLimit + 1,
- mediaOnly: this.mediaOnly,
+ withFiles: this.mediaOnly,
untilId: (this.$refs.timeline as any).tail().id,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
@@ -138,7 +138,7 @@ export default Vue.extend({
},
onNote(note) {
- if (this.mediaOnly && note.media.length == 0) return;
+ if (this.mediaOnly && note.files.length == 0) return;
// Prepend a note
(this.$refs.timeline as any).prepend(note);
diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue
index 26b989656e..aafe9a45d3 100644
--- a/src/client/app/desktop/views/pages/deck/deck.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.vue
@@ -85,6 +85,7 @@ export default Vue.extend({
},
mounted() {
+ document.title = (this as any).os.instanceName;
document.documentElement.style.overflow = 'hidden';
},
@@ -138,6 +139,15 @@ export default Vue.extend({
});
}
}, {
+ icon: '%fa:at%',
+ text: '%i18n:common.deck.mentions%',
+ action: () => {
+ this.$store.dispatch('settings/addDeckColumn', {
+ id: uuid(),
+ type: 'mentions'
+ });
+ }
+ }, {
icon: '%fa:list%',
text: '%i18n:common.deck.list%',
action: () => {
@@ -152,6 +162,20 @@ export default Vue.extend({
});
}
}, {
+ icon: '%fa:hashtag%',
+ text: '%i18n:common.deck.hashtag%',
+ action: () => {
+ (this as any).apis.input({
+ title: '%i18n:@enter-hashtag-tl-title%'
+ }).then(title => {
+ this.$store.dispatch('settings/addDeckColumn', {
+ id: uuid(),
+ type: 'hashtag',
+ tagTlId: this.$store.state.settings.tagTimelines.find(x => x.title == title).id
+ });
+ });
+ }
+ }, {
icon: '%fa:bell R%',
text: '%i18n:common.deck.notifications%',
action: () => {
diff --git a/src/client/app/desktop/views/pages/drive.vue b/src/client/app/desktop/views/pages/drive.vue
index 217dcb7751..dec6c4551a 100644
--- a/src/client/app/desktop/views/pages/drive.vue
+++ b/src/client/app/desktop/views/pages/drive.vue
@@ -31,7 +31,7 @@ export default Vue.extend({
const title = folder.name + ' | %i18n:@title%';
// Rewrite URL
- history.pushState(null, title, '/i/drive/folder/' + folder.id);
+ history.pushState(null, title, `/i/drive/folder/${folder.id}`);
document.title = title;
}
diff --git a/src/client/app/desktop/views/pages/games/reversi.vue b/src/client/app/desktop/views/pages/games/reversi.vue
index ce9b42c65f..1b0e790a22 100644
--- a/src/client/app/desktop/views/pages/games/reversi.vue
+++ b/src/client/app/desktop/views/pages/games/reversi.vue
@@ -16,10 +16,10 @@ export default Vue.extend({
methods: {
nav(game, actualNav) {
if (actualNav) {
- this.$router.push('/reversi/' + game.id);
+ this.$router.push(`/reversi/${game.id}`);
} else {
// TODO: https://github.com/vuejs/vue-router/issues/703
- this.$router.push('/reversi/' + game.id);
+ this.$router.push(`/reversi/${game.id}`);
}
}
}
diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue
index 1ebd53cef4..4be33dda04 100644
--- a/src/client/app/desktop/views/pages/messaging-room.vue
+++ b/src/client/app/desktop/views/pages/messaging-room.vue
@@ -46,7 +46,7 @@ export default Vue.extend({
this.user = user;
this.fetching = false;
- document.title = 'メッセージ: ' + getUserName(this.user);
+ document.title = `メッセージ: ${getUserName(this.user)}`;
Progress.done();
});
diff --git a/src/client/app/desktop/views/pages/stats/stats.vue b/src/client/app/desktop/views/pages/stats/stats.vue
index 41005b6398..7a4e4ab5ce 100644
--- a/src/client/app/desktop/views/pages/stats/stats.vue
+++ b/src/client/app/desktop/views/pages/stats/stats.vue
@@ -43,7 +43,7 @@ export default Vue.extend({
> .stats
display flex
justify-content center
- margin-bottom 16px
+ margin 0 auto 16px auto
padding 32px
background #fff
box-shadow 0 2px 8px rgba(#000, 0.1)
@@ -60,5 +60,6 @@ export default Vue.extend({
font-size 70%
> div
- max-width 850px
+ max-width 950px
+ margin 0 auto
</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 e4a771910a..0e7e3f1d77 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
@@ -1,5 +1,5 @@
<template>
-<div class="followers-you-know">
+<div class="vahgrswmbzfdlmomxnqftuueyvwaafth">
<p class="title">%fa:users%%i18n:@title%</p>
<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
<div v-if="!fetching && users.length > 0">
@@ -36,8 +36,8 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.followers-you-know
- background #fff
+root(isDark)
+ background isDark ? #282C37 : #fff
border solid 1px rgba(#000, 0.075)
border-radius 6px
@@ -48,7 +48,7 @@ export default Vue.extend({
line-height 42px
font-size 0.9em
font-weight bold
- color #888
+ color isDark ? #e3e5e8 : #888
box-shadow 0 1px rgba(#000, 0.07)
> i
@@ -77,4 +77,10 @@ export default Vue.extend({
> i
margin-right 4px
+.vahgrswmbzfdlmomxnqftuueyvwaafth[data-darkmode]
+ root(true)
+
+.vahgrswmbzfdlmomxnqftuueyvwaafth:not([data-darkmode])
+ root(false)
+
</style>
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 516eea0288..a238565588 100644
--- a/src/client/app/desktop/views/pages/user/user.friends.vue
+++ b/src/client/app/desktop/views/pages/user/user.friends.vue
@@ -1,5 +1,5 @@
<template>
-<div class="friends">
+<div class="hozptpaliadatkehcmcayizwzwwctpbc">
<p class="title">%fa:users%%i18n:@title%</p>
<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
<template v-if="!fetching && users.length != 0">
@@ -41,7 +41,6 @@ export default Vue.extend({
<style lang="stylus" scoped>
root(isDark)
-.friends
background isDark ? #282C37 : #fff
border solid 1px rgba(#000, 0.075)
border-radius 6px
@@ -113,10 +112,10 @@ root(isDark)
top 16px
right 16px
-.friends[data-darkmode]
+.hozptpaliadatkehcmcayizwzwwctpbc[data-darkmode]
root(true)
-.friends:not([data-darkmode])
+.hozptpaliadatkehcmcayizwzwwctpbc:not([data-darkmode])
root(false)
</style>
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 d8f4656ed0..4b434ec219 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -6,7 +6,7 @@
<div class="title">
<p class="name">{{ user | userName }}</p>
<div>
- <span class="username"><mk-acct :user="user"/></span>
+ <span class="username"><mk-acct :user="user" :detail="true" /></span>
<span v-if="user.isBot" title="%i18n:@is-bot%">%fa:robot%</span>
<span class="location" v-if="user.host === null && user.profile.location">%fa:map-marker% {{ user.profile.location }}</span>
<span class="birthday" v-if="user.host === null && user.profile.birthday">%fa:birthday-cake% {{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</span>
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 8397e56484..c5cd9e24fe 100644
--- a/src/client/app/desktop/views/pages/user/user.photos.vue
+++ b/src/client/app/desktop/views/pages/user/user.photos.vue
@@ -1,5 +1,5 @@
<template>
-<div class="photos">
+<div class="dzsuvbsrrrwobdxifudxuefculdfiaxd">
<p class="title">%fa:camera%%i18n:@title%</p>
<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
<div class="stream" v-if="!fetching && images.length > 0">
@@ -24,12 +24,12 @@ export default Vue.extend({
mounted() {
(this as any).api('users/notes', {
userId: this.user.id,
- withMedia: true,
+ withFiles: true,
limit: 9
}).then(notes => {
notes.forEach(note => {
- note.media.forEach(media => {
- if (this.images.length < 9) this.images.push(media);
+ note.files.forEach(file => {
+ if (this.images.length < 9) this.images.push(file);
});
});
this.fetching = false;
@@ -40,7 +40,6 @@ export default Vue.extend({
<style lang="stylus" scoped>
root(isDark)
-.photos
background isDark ? #282C37 : #fff
border solid 1px rgba(#000, 0.075)
border-radius 6px
@@ -88,10 +87,10 @@ root(isDark)
> i
margin-right 4px
-.photos[data-darkmode]
+.dzsuvbsrrrwobdxifudxuefculdfiaxd[data-darkmode]
root(true)
-.photos:not([data-darkmode])
+.dzsuvbsrrrwobdxifudxuefculdfiaxd:not([data-darkmode])
root(false)
</style>
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 67987fcb94..54221380a7 100644
--- a/src/client/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/client/app/desktop/views/pages/user/user.timeline.vue
@@ -66,7 +66,7 @@ export default Vue.extend({
limit: fetchLimit + 1,
untilDate: this.date ? this.date.getTime() : undefined,
includeReplies: this.mode == 'with-replies',
- withMedia: this.mode == 'with-media'
+ withFiles: this.mode == 'with-media'
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
@@ -86,7 +86,7 @@ export default Vue.extend({
userId: this.user.id,
limit: fetchLimit + 1,
includeReplies: this.mode == 'with-replies',
- withMedia: this.mode == 'with-media',
+ withFiles: this.mode == 'with-media',
untilId: (this.$refs.timeline as any).tail().id
});
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index ac2f921a21..ea1734f8c7 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -1,45 +1,145 @@
<template>
<div class="mk-welcome">
- <img ref="pointer" class="pointer" src="/assets/pointer.png" alt="">
<button @click="dark">
<template v-if="$store.state.device.darkmode">%fa:moon%</template>
<template v-else>%fa:R moon%</template>
</button>
- <div class="body">
- <div class="container">
- <div class="info">
- <span><b>{{ host }}</b></span>
- <span class="stats" v-if="stats">
- <span>%fa:user% {{ stats.originalUsersCount | number }}</span>
- <span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
- </span>
- </div>
- <main>
- <div class="about">
+
+ <mk-forkit class="forkit"/>
+
+ <main>
+ <div class="body">
+ <div class="main block">
+ <div>
<h1 v-if="name != 'Misskey'">{{ name }}</h1>
<h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"></h1>
- <p class="powerd-by" v-if="name != 'Misskey'" v-html="'%i18n:@powered-by-misskey%'"></p>
- <p class="desc" v-html="description || '%i18n:common.about%'"></p>
- <a ref="signup" @click="signup">📦 %i18n:@signup%</a>
+
+ <div class="info">
+ <span><b>{{ host }}</b> - <span v-html="'%i18n:@powered-by-misskey%'"></span></span>
+ <span class="stats" v-if="stats">
+ <span>%fa:user% {{ stats.originalUsersCount | number }}</span>
+ <span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
+ </span>
+ </div>
+
+ <div class="desc">
+ <span class="desc" v-html="description || '%i18n:common.about%'"></span>
+ <a class="about" @click="about">%i18n:@about%</a>
+ </div>
+
+ <p class="sign">
+ <span class="signup" @click="signup">%i18n:@signup%</span>
+ <span class="divider">|</span>
+ <span class="signin" @click="signin">%i18n:@signin%</span>
+ </p>
+
+ <img src="/assets/ai.png" alt="" title="藍" class="char">
+ </div>
+ </div>
+
+ <div class="announcements block">
+ <header>%fa:broadcast-tower% %i18n:@announcements%</header>
+ <div v-if="announcements && announcements.length > 0">
+ <div v-for="announcement in announcements">
+ <h1 v-html="announcement.title"></h1>
+ <div v-html="announcement.text"></div>
+ </div>
+ </div>
+ </div>
+
+ <div class="photos block">
+ <header>%fa:images% %i18n:@photos%</header>
+ <div>
+ <div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div>
</div>
- <div class="login">
- <mk-signin/>
+ </div>
+
+ <div class="tag-cloud block">
+ <div>
+ <mk-tag-cloud/>
+ </div>
+ </div>
+
+ <div class="nav block">
+ <div>
+ <mk-nav class="nav"/>
+ </div>
+ </div>
+
+ <div class="side">
+ <div class="trends block">
+ <div>
+ <mk-trends/>
+ </div>
+ </div>
+
+ <div class="tl block">
+ <header>%fa:comment-alt R% %i18n:@timeline%</header>
+ <div>
+ <mk-welcome-timeline class="tl" :max="20"/>
+ </div>
+ </div>
+
+ <div class="info block">
+ <header>%fa:info-circle% %i18n:@info%</header>
+ <div>
+ <div v-if="meta" class="body">
+ <p>Version: <b>{{ meta.version }}</b></p>
+ <p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p>
+ </div>
+ </div>
</div>
- </main>
- <div class="hashtags">
- <router-link v-for="tag in tags" :key="tag" :to="`/tags/${ tag }`" :title="tag">#{{ tag }}</router-link>
</div>
- <mk-nav class="nav"/>
</div>
- <mk-forkit class="forkit"/>
- <img src="assets/title.dark.svg" :alt="name">
- </div>
- <div class="tl">
- <mk-welcome-timeline :max="20"/>
- </div>
- <modal name="signup" width="500px" height="auto" scrollable>
- <header :class="$style.signupFormHeader">%i18n:@signup%</header>
- <mk-signup :class="$style.signupForm"/>
+ </main>
+
+ <modal name="about" :class="$store.state.device.darkmode ? ['about', 'modal-dark'] : ['about', 'modal-light']" width="800px" height="auto" scrollable>
+ <article class="fpdezooorhntlzyeszemrsqdlgbysvxq">
+ <h1>%i18n:common.intro.title%</h1>
+ <p v-html="'%i18n:common.intro.about%'"></p>
+ <section>
+ <h2>%i18n:common.intro.features%</h2>
+ <section>
+ <div class="body">
+ <h3>%i18n:common.intro.rich-contents%</h3>
+ <p v-html="'%i18n:common.intro.rich-contents-desc%'"></p>
+ </div>
+ <div class="image"><img src="/assets/about/post.png" alt=""></div>
+ </section>
+ <section>
+ <div class="body">
+ <h3>%i18n:common.intro.reaction%</h3>
+ <p v-html="'%i18n:common.intro.reaction-desc%'"></p>
+ </div>
+ <div class="image"><img src="/assets/about/reaction.png" alt=""></div>
+ </section>
+ <section>
+ <div class="body">
+ <h3>%i18n:common.intro.ui%</h3>
+ <p v-html="'%i18n:common.intro.ui-desc%'"></p>
+ </div>
+ <div class="image"><img src="/assets/about/ui.png" alt=""></div>
+ </section>
+ <section>
+ <div class="body">
+ <h3>%i18n:common.intro.drive%</h3>
+ <p v-html="'%i18n:common.intro.drive-desc%'"></p>
+ </div>
+ <div class="image"><img src="/assets/about/drive.png" alt=""></div>
+ </section>
+ </section>
+ <p v-html="'%i18n:common.intro.outro%'"></p>
+ </article>
+ </modal>
+
+ <modal name="signup" :class="$store.state.device.darkmode ? 'modal-dark' : 'modal-light'" width="450px" height="auto" scrollable>
+ <header class="formHeader">%i18n:@signup%</header>
+ <mk-signup class="form"/>
+ </modal>
+
+ <modal name="signin" :class="$store.state.device.darkmode ? 'modal-dark' : 'modal-light'" width="450px" height="auto" scrollable>
+ <header class="formHeader">%i18n:@signin%</header>
+ <mk-signin class="form"/>
</modal>
</div>
</template>
@@ -47,52 +147,62 @@
<script lang="ts">
import Vue from 'vue';
import { host, copyright } from '../../../config';
+import { concat } from '../../../../../prelude/array';
export default Vue.extend({
data() {
return {
+ meta: null,
stats: null,
copyright,
host,
name: 'Misskey',
description: '',
- pointerInterval: null,
- tags: []
+ announcements: [],
+ photos: []
};
},
+
created() {
(this as any).os.getMeta().then(meta => {
+ this.meta = meta;
this.name = meta.name;
this.description = meta.description;
+ this.announcements = meta.broadcasts;
});
(this as any).api('stats').then(stats => {
this.stats = stats;
});
- (this as any).api('hashtags/trend').then(stats => {
- this.tags = stats.map(x => x.tag);
+ const image = [
+ 'image/jpeg',
+ 'image/png',
+ 'image/gif'
+ ];
+
+ (this as any).api('notes/local-timeline', {
+ fileType: image,
+ limit: 6
+ }).then((notes: any[]) => {
+ const files = concat(notes.map((n: any): any[] => n.files));
+ this.photos = files.filter(f => image.includes(f.type)).slice(0, 6);
});
},
- mounted() {
- this.point();
- this.pointerInterval = setInterval(this.point, 100);
- },
- beforeDestroy() {
- clearInterval(this.pointerInterval);
- },
+
methods: {
- point() {
- const x = this.$refs.signup.getBoundingClientRect();
- this.$refs.pointer.style.top = x.top + x.height + 'px';
- this.$refs.pointer.style.left = x.left + 'px';
+ about() {
+ this.$modal.show('about');
},
+
signup() {
this.$modal.show('signup');
},
+
signin() {
this.$modal.show('signin');
},
+
dark() {
this.$store.commit('device/set', {
key: 'darkmode',
@@ -103,11 +213,88 @@ export default Vue.extend({
});
</script>
-<style>
-#wait {
- right: auto;
- left: 15px;
-}
+<style lang="stylus">
+#wait
+ right auto
+ left 15px
+
+.v--modal-overlay
+ background rgba(0, 0, 0, 0.6)
+
+.modal-light
+ .v--modal-box
+ color #777
+
+ .formHeader
+ border-bottom solid 1px #eee
+
+.modal-dark
+ .v--modal-box
+ background #313543
+ color #fff
+
+ .formHeader
+ border-bottom solid 1px rgba(#000, 0.2)
+
+.modal-light
+.modal-dark
+ .form
+ padding 24px 48px 48px 48px
+
+ .formHeader
+ text-align center
+ padding 48px 0 12px 0
+ margin 0 48px
+ font-size 1.5em
+
+.v--modal-overlay.about
+ .v--modal-box.v--modal
+ margin 32px 0
+
+.fpdezooorhntlzyeszemrsqdlgbysvxq
+ padding 64px
+
+ > p:last-child
+ margin-bottom 0
+
+ > h1
+ margin-top 0
+
+ > section
+ > h2
+ border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
+
+ > section
+ display grid
+ grid-template-rows 1fr
+ grid-template-columns 180px 1fr
+ gap 32px
+ margin-bottom 32px
+ padding-bottom 32px
+ border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
+
+ &:nth-child(odd)
+ grid-template-columns 1fr 180px
+
+ > .body
+ grid-column 1
+
+ > .image
+ grid-column 2
+
+ > .body
+ grid-row 1
+ grid-column 2
+
+ > .image
+ grid-row 1
+ grid-column 1
+
+ > img
+ display block
+ width 100%
+ height 100%
+ object-fit cover
</style>
<style lang="stylus" scoped>
@@ -116,176 +303,200 @@ export default Vue.extend({
root(isDark)
display flex
min-height 100vh
+ //background-color #00070F
+ //background-image url('/assets/bg.jpg')
+ //background-position center
+ //background-size cover
- > .pointer
- display block
+ > .forkit
position absolute
- z-index 1
top 0
right 0
- width 180px
- margin 0 0 0 -180px
- transform rotateY(180deg) translateX(-10px) translateY(-48px)
- pointer-events none
> button
position fixed
z-index 1
- top 0
- left 0
+ bottom 16px
+ left 16px
padding 16px
font-size 18px
- color #fff
+ color isDark ? #fff : #444
- display none // TODO
+ > main
+ margin 0 auto
+ padding 64px
+ width 100%
+ max-width 1200px
- > .body
- flex 1
- padding 64px 0 0 0
- text-align center
- background #578394
- background-position center
- background-size cover
-
- &:before
- content ''
- display block
- position absolute
- top 0
- left 0
- right 0
- bottom 0
- background rgba(#000, 0.5)
-
- > .forkit
- position absolute
- top 0
- right 0
+ .block
+ color isDark ? #fff : #444
+ background isDark ? #282C37 : #fff
+ box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
+ //border-radius 8px
+ overflow auto
- > img
- position absolute
- bottom 16px
- right 16px
- width 150px
+ > header
+ z-index 1
+ padding 0 16px
+ line-height 48px
+ background isDark ? #313543 : #fff
- > .container
- $aboutWidth = 380px
- $loginWidth = 340px
- $width = $aboutWidth + $loginWidth
+ if !isDark
+ box-shadow 0 1px 0px rgba(0, 0, 0, 0.1)
- > .info
- margin 0 auto 16px auto
- width $width
- font-size 14px
- color #fff
+ & + div
+ max-height calc(100% - 48px)
- > .stats
- margin-left 16px
- padding-left 16px
- border-left solid 1px #fff
+ > div
+ overflow auto
- > *
- margin-right 16px
+ > .body
+ display grid
+ grid-template-rows 390px 1fr 256px 64px
+ grid-template-columns 1fr 1fr 350px
+ gap 16px
+ height 1150px
- > main
- display flex
- margin auto
- width $width
- border-radius 8px
- overflow hidden
- box-shadow 0 2px 8px rgba(#000, 0.3)
+ > .main
+ grid-row 1
+ grid-column 1 / 3
+ border-top solid 5px $theme-color
- > .about
- width $aboutWidth
- color #444
- background #fff
+ > div
+ padding 32px
+ min-height 100%
> h1
- margin 0 0 16px 0
- padding 32px 32px 0 32px
- color #444
+ margin 0
> img
- width 170px
- vertical-align bottom
+ margin -8px 0 0 -16px
+ max-width 280px
- > .powerd-by
- margin 16px
- opacity 0.7
+ > .info
+ margin 0 auto 16px auto
+ width $width
+ font-size 14px
+
+ > .stats
+ margin-left 16px
+ padding-left 16px
+ border-left solid 1px isDark ? #fff : #444
+
+ > *
+ margin-right 16px
> .desc
- margin 0
- padding 0 32px 16px 32px
+ max-width calc(100% - 150px)
- > a
- display inline-block
- margin 0 0 32px 0
- font-weight bold
+ > .sign
+ font-size 120%
+ margin-bottom 0
- > .login
- width $loginWidth
- padding 16px 32px 32px 32px
- background isDark ? #2e3440 : #f5f5f5
+ > .divider
+ margin 0 16px
- > .hashtags
- margin 16px auto
- width $width
- font-size 14px
- color #fff
- background rgba(#000, 0.3)
- border-radius 8px
+ > .signin
+ > .signup
+ cursor pointer
- > *
- display inline-block
- margin 14px
+ &:hover
+ color $theme-color
+
+ > .char
+ display block
+ position absolute
+ right 16px
+ bottom 0
+ height 320px
+ opacity 0.7
+
+ > *:not(.char)
+ z-index 1
+
+ > .announcements
+ grid-row 2
+ grid-column 1
+
+ > div
+ padding 32px
+
+ > div
+ padding 0 0 16px 0
+ margin 0 0 16px 0
+ border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
+
+ > h1
+ margin 0
+ font-size 1.25em
+
+ > .photos
+ grid-row 2
+ grid-column 2
+
+ > div
+ display grid
+ grid-template-rows 1fr 1fr 1fr
+ grid-template-columns 1fr 1fr
+ gap 8px
+ height 100%
+ padding 16px
+
+ > div
+ //border-radius 4px
+ background-position center center
+ background-size cover
+
+ > .tag-cloud
+ grid-row 3
+ grid-column 1 / 3
+
+ > div
+ height 256px
+ padding 32px
> .nav
- display block
- margin 16px 0
+ display flex
+ justify-content center
+ align-items center
+ grid-row 4
+ grid-column 1 / 3
font-size 14px
- color #fff
-
- > .tl
- margin 0
- width 410px
- height 100vh
- text-align left
- background isDark ? #313543 : #fff
- > *
- max-height 100%
- overflow auto
+ > .side
+ display grid
+ grid-row 1 / 5
+ grid-column 3
+ grid-template-rows 1fr 350px
+ grid-template-columns 1fr
+ gap 16px
-.mk-welcome[data-darkmode]
- root(true)
+ > .tl
+ grid-row 1
+ grid-column 1
+ overflow auto
-.mk-welcome:not([data-darkmode])
- root(false)
+ > .trends
+ grid-row 2
+ grid-column 1
+ padding 8px
-</style>
+ > .info
+ grid-row 3
+ grid-column 1
-<style lang="stylus" module>
-.signupForm
- padding 24px 48px 48px 48px
+ > div
+ padding 16px
-.signupFormHeader
- padding 48px 0 12px 0
- margin: 0 48px
- font-size 1.5em
- color #777
- border-bottom solid 1px #eee
+ > .body
+ > p
+ display block
+ margin 0
-.signinForm
- padding 24px 48px 48px 48px
+.mk-welcome[data-darkmode]
+ root(true)
-.signinFormHeader
- padding 48px 0 12px 0
- margin: 0 48px
- font-size 1.5em
- color #777
- border-bottom solid 1px #eee
+.mk-welcome:not([data-darkmode])
+ root(false)
-.nav
- a
- color #666
</style>
diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue
index c33bf2f2f2..aeaab63ac4 100644
--- a/src/client/app/desktop/views/widgets/trends.vue
+++ b/src/client/app/desktop/views/widgets/trends.vue
@@ -49,7 +49,7 @@ export default define({
offset: this.offset,
renote: false,
reply: false,
- media: false,
+ file: false,
poll: false
}).then(notes => {
const note = notes ? notes[0] : null;
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
index cf97957400..db3852da60 100644
--- a/src/client/app/init.ts
+++ b/src/client/app/init.ts
@@ -5,31 +5,20 @@
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';
import VAnimateCss from 'v-animate-css';
-import Element from 'element-ui';
-import ElementLocaleEn from 'element-ui/lib/locale/lang/en';
-import ElementLocaleJa from 'element-ui/lib/locale/lang/ja';
+import VModal from 'vue-js-modal';
import App from './app.vue';
import checkForUpdate from './common/scripts/check-for-update';
import MiOS, { API } from './mios';
import { version, codename, lang } from './config';
-let elementLocale;
-switch (lang) {
- case 'ja-JP': elementLocale = ElementLocaleJa; break;
- case 'en-US': elementLocale = ElementLocaleEn; break;
- default: elementLocale = ElementLocaleEn; break;
-}
-
Vue.use(Vuex);
Vue.use(VueRouter);
-Vue.use(VModal);
Vue.use(TreeView);
Vue.use(VAnimateCss);
-Vue.use(Element, { locale: elementLocale });
+Vue.use(VModal);
// Register global directives
require('./common/views/directives');
@@ -42,9 +31,13 @@ require('./common/views/widgets');
require('./common/views/filters');
Vue.mixin({
- destroyed(this: any) {
- if (this.$el.parentNode) {
- this.$el.parentNode.removeChild(this.$el);
+ methods: {
+ destroyDom() {
+ this.$destroy();
+
+ if (this.$el.parentNode) {
+ this.$el.parentNode.removeChild(this.$el);
+ }
}
}
});
diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts
index 664848b5e7..0f72cd2f34 100644
--- a/src/client/app/mios.ts
+++ b/src/client/app/mios.ts
@@ -3,7 +3,7 @@ import { EventEmitter } from 'eventemitter3';
import * as uuid from 'uuid';
import initStore from './store';
-import { apiUrl, swPublickey, version, lang, googleMapsApiKey } from './config';
+import { apiUrl, version, lang } from './config';
import Progress from './common/scripts/loading';
import Connection from './common/scripts/streaming/stream';
import { HomeStreamManager } from './common/scripts/streaming/home';
@@ -17,6 +17,7 @@ import Err from './common/views/components/connect-failed.vue';
import { LocalTimelineStreamManager } from './common/scripts/streaming/local-timeline';
import { HybridTimelineStreamManager } from './common/scripts/streaming/hybrid-timeline';
import { GlobalTimelineStreamManager } from './common/scripts/streaming/global-timeline';
+import { erase } from '../../prelude/array';
//#region api requests
let spinner = null;
@@ -230,13 +231,13 @@ export default class MiOS extends EventEmitter {
//#region Init stream managers
this.streams.serverStatsStream = new ServerStatsStreamManager(this);
this.streams.notesStatsStream = new NotesStatsStreamManager(this);
+ this.streams.localTimelineStream = new LocalTimelineStreamManager(this, this.store.state.i);
this.once('signedin', () => {
// Init home stream manager
this.stream = new HomeStreamManager(this, this.store.state.i);
// Init other stream manager
- this.streams.localTimelineStream = new LocalTimelineStreamManager(this, this.store.state.i);
this.streams.hybridTimelineStream = new HybridTimelineStreamManager(this, this.store.state.i);
this.streams.globalTimelineStream = new GlobalTimelineStreamManager(this, this.store.state.i);
this.streams.driveStream = new DriveStreamManager(this, this.store.state.i);
@@ -361,7 +362,7 @@ export default class MiOS extends EventEmitter {
// A public key your push server will use to send
// messages to client apps via a push server.
- applicationServerKey: urlBase64ToUint8Array(swPublickey)
+ applicationServerKey: urlBase64ToUint8Array(this.meta.data.swPublickey)
};
// Subscribe push notification
@@ -537,7 +538,7 @@ export default class MiOS extends EventEmitter {
}
public unregisterStreamConnection(connection: Connection) {
- this.connections = this.connections.filter(c => c != connection);
+ this.connections = erase(connection, this.connections);
}
}
diff --git a/src/client/app/mobile/api/post.ts b/src/client/app/mobile/api/post.ts
index 15b2f6b691..5c0f0af852 100644
--- a/src/client/app/mobile/api/post.ts
+++ b/src/client/app/mobile/api/post.ts
@@ -1,13 +1,12 @@
-import PostForm from '../views/components/post-form.vue';
+import PostForm from '../views/components/post-form-dialog.vue';
export default (os) => (opts) => {
const o = opts || {};
- const app = document.getElementById('app');
- app.style.display = 'none';
+ document.documentElement.style.overflow = 'hidden';
function recover() {
- app.style.display = 'block';
+ document.documentElement.style.overflow = 'auto';
}
const vm = new PostForm({
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index 5b9d45462a..9412c85980 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -6,7 +6,6 @@ import VueRouter from 'vue-router';
// Style
import './style.styl';
-import '../../element.scss';
import init from '../init';
diff --git a/src/client/app/mobile/views/components/dialog.vue b/src/client/app/mobile/views/components/dialog.vue
index 9ee01cb782..6a0d74c752 100644
--- a/src/client/app/mobile/views/components/dialog.vue
+++ b/src/client/app/mobile/views/components/dialog.vue
@@ -78,7 +78,7 @@ export default Vue.extend({
scale: 0.8,
duration: 300,
easing: [ 0.5, -0.5, 1, 0.5 ],
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
},
onBgClick() {
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 d95d5fa223..92ac211af2 100644
--- a/src/client/app/mobile/views/components/drive-file-chooser.vue
+++ b/src/client/app/mobile/views/components/drive-file-chooser.vue
@@ -1,12 +1,12 @@
<template>
-<div class="mk-drive-file-chooser">
+<div class="cdxzvcfawjxdyxsekbxbfgtplebnoneb">
<div class="body">
<header>
<h1>%i18n:@select-file%<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1>
<button class="close" @click="cancel">%fa:times%</button>
<button v-if="multiple" class="ok" @click="ok">%fa:check%</button>
</header>
- <mk-drive ref="browser"
+ <mk-drive class="drive" ref="browser"
:select-file="true"
:multiple="multiple"
@change-selection="onChangeSelection"
@@ -31,24 +31,24 @@ export default Vue.extend({
},
onSelected(file) {
this.$emit('selected', file);
- this.$destroy();
+ this.destroyDom();
},
cancel() {
this.$emit('canceled');
- this.$destroy();
+ this.destroyDom();
},
ok() {
this.$emit('selected', this.files);
- this.$destroy();
+ this.destroyDom();
}
}
});
</script>
<style lang="stylus" scoped>
-.mk-drive-file-chooser
+root(isDark)
position fixed
- z-index 2048
+ z-index 20000
top 0
left 0
width 100%
@@ -59,10 +59,11 @@ export default Vue.extend({
> .body
width 100%
height 100%
- background #fff
+ background isDark ? #282c37 : #fff
> header
- border-bottom solid 1px #eee
+ border-bottom solid 1px isDark ? #1b1f29 : #eee
+ color isDark ? #fff : #111
> h1
margin 0
@@ -90,9 +91,15 @@ export default Vue.extend({
line-height 42px
width 42px
- > .mk-drive
+ > .drive
height calc(100% - 42px)
overflow scroll
-webkit-overflow-scrolling touch
+.cdxzvcfawjxdyxsekbxbfgtplebnoneb[data-darkmode]
+ root(true)
+
+.cdxzvcfawjxdyxsekbxbfgtplebnoneb:not([data-darkmode])
+ root(false)
+
</style>
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 7934fb7816..6d3fba1efd 100644
--- a/src/client/app/mobile/views/components/drive-folder-chooser.vue
+++ b/src/client/app/mobile/views/components/drive-folder-chooser.vue
@@ -19,11 +19,11 @@ export default Vue.extend({
methods: {
cancel() {
this.$emit('canceled');
- this.$destroy();
+ this.destroyDom();
},
ok() {
this.$emit('selected', (this.$refs.browser as any).folder);
- this.$destroy();
+ this.destroyDom();
}
}
});
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 deb9941be8..8108892597 100644
--- a/src/client/app/mobile/views/components/drive.file-detail.vue
+++ b/src/client/app/mobile/views/components/drive.file-detail.vue
@@ -1,5 +1,5 @@
<template>
-<div class="file-detail">
+<div class="pyvicwrksnfyhpfgkjwqknuururpaztw">
<div class="preview">
<img v-if="kind == 'image'" ref="img"
:src="file.url"
@@ -25,7 +25,7 @@
</div>
<div class="info">
<div>
- <span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span>
+ <span class="type"><mk-file-type-icon :type="file.type"/> {{ file.type }}</span>
<span class="separator"></span>
<span class="data-size">{{ file.datasize | bytes }}</span>
<span class="separator"></span>
@@ -67,7 +67,7 @@
import Vue from 'vue';
import * as EXIF from 'exif-js';
import * as hljs from 'highlight.js';
-import gcd from '../../../common/scripts/gcd';
+import { gcd } from '../../../../../prelude/math';
export default Vue.extend({
props: ['file'],
@@ -134,11 +134,10 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.file-detail
-
+root(isDark)
> .preview
padding 8px
- background #f0f0f0
+ background isDark ? #191b22 : #f0f0f0
> img
display block
@@ -150,7 +149,7 @@ export default Vue.extend({
> footer
padding 8px 8px 0 8px
font-size 0.8em
- color #888
+ color isDark ? #606984 : #888
text-align center
> .separator
@@ -179,25 +178,17 @@ export default Vue.extend({
> .info
padding 14px
font-size 0.8em
- border-top solid 1px #dfdfdf
+ border-top solid 1px isDark ? #1c2023 : #dfdfdf
> div
max-width 500px
margin 0 auto
+ color isDark ? #9397a2 : #9d9d9d
> .separator
padding 0 4px
- color #cdcdcd
-
- > .type
- > .data-size
- color #9d9d9d
-
- > mk-file-type-icon
- margin-right 4px
> .created-at
- color #bdbdbd
> [data-fa]
margin-right 2px
@@ -207,7 +198,7 @@ export default Vue.extend({
> .menu
padding 14px
- border-top solid 1px #dfdfdf
+ border-top solid 1px isDark ? #1c2023 : #dfdfdf
> div
max-width 500px
@@ -218,14 +209,14 @@ export default Vue.extend({
width 100%
padding 10px 16px
margin 0 0 12px 0
- color #333
+ color isDark ? #dfe3e8 : #333
font-size 0.9em
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
+ text-shadow 0 1px 0 isDark ? rgba(0, 0, 0, 0.9) : rgba(255, 255, 255, 0.9)
+ background-image isDark ? linear-gradient(#292f3c, #1b2025) : linear-gradient(#fafafa, #eaeaea)
+ border 1px solid isDark ? #121417 : #ddd
+ border-bottom-color isDark ? #060606 : #cecece
border-radius 3px
&:last-child
@@ -242,7 +233,7 @@ export default Vue.extend({
> .hash
padding 14px
- border-top solid 1px #dfdfdf
+ border-top solid 1px isDark ? #1c2023 : #dfdfdf
> div
max-width 500px
@@ -252,7 +243,7 @@ export default Vue.extend({
display block
margin 0
padding 0
- color #555
+ color isDark ? #a8b7d0 : #555
font-size 0.9em
> [data-fa]
@@ -273,7 +264,7 @@ export default Vue.extend({
> .exif
padding 14px
- border-top solid 1px #dfdfdf
+ border-top solid 1px isDark ? #1c2023 : #dfdfdf
> div
max-width 500px
@@ -283,7 +274,7 @@ export default Vue.extend({
display block
margin 0
padding 0
- color #555
+ color isDark ? #a8b7d0 : #555
font-size 0.9em
> [data-fa]
@@ -301,4 +292,10 @@ export default Vue.extend({
border-radius 2px
background #f5f5f5
+.pyvicwrksnfyhpfgkjwqknuururpaztw[data-darkmode]
+ root(true)
+
+.pyvicwrksnfyhpfgkjwqknuururpaztw:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue
index 6dec4b9f4f..4375cfdd7b 100644
--- a/src/client/app/mobile/views/components/drive.file.vue
+++ b/src/client/app/mobile/views/components/drive.file.vue
@@ -1,5 +1,5 @@
<template>
-<a class="file" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected">
+<a class="vupkuhvjnjyqaqhsiogfbywvjxynrgsm" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected">
<div class="container">
<div class="thumbnail" :style="thumbnail"></div>
<div class="body">
@@ -7,20 +7,12 @@
<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
<span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
</p>
- <!--
- if file.tags.length > 0
- ul.tags
- each tag in file.tags
- li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name
- -->
<footer>
<span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span>
<span class="separator"></span>
<span class="data-size">{{ file.datasize | bytes }}</span>
<span class="separator"></span>
- <span class="created-at">
- %fa:R clock%<mk-time :time="file.createdAt"/>
- </span>
+ <span class="created-at">%fa:R clock%<mk-time :time="file.createdAt"/></span>
<template v-if="file.isSensitive">
<span class="separator"></span>
<span class="nsfw">%fa:eye-slash% %i18n:@nsfw%</span>
@@ -73,7 +65,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
@import '~const.styl'
-.file
+root(isDark)
display block
text-decoration none !important
@@ -111,7 +103,7 @@ export default Vue.extend({
padding 0
font-size 0.9em
font-weight bold
- color #555
+ color isDark ? #fff : #555
text-overflow ellipsis
overflow-wrap break-word
@@ -138,7 +130,6 @@ export default Vue.extend({
> .separator
padding 0 4px
- color #CDCDCD
> .type
color #9D9D9D
@@ -164,4 +155,10 @@ export default Vue.extend({
&, *
color #fff !important
+.vupkuhvjnjyqaqhsiogfbywvjxynrgsm[data-darkmode]
+ root(true)
+
+.vupkuhvjnjyqaqhsiogfbywvjxynrgsm:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/mobile/views/components/drive.folder.vue b/src/client/app/mobile/views/components/drive.folder.vue
index 22ff38fecb..f76ecba6ad 100644
--- a/src/client/app/mobile/views/components/drive.folder.vue
+++ b/src/client/app/mobile/views/components/drive.folder.vue
@@ -1,5 +1,5 @@
<template>
-<a class="root folder" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`">
+<a class="jvwxssxsytqlqvrpiymarjlzlsxskqsr" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`">
<div class="container">
<p class="name">%fa:folder%{{ folder.name }}</p>%fa:angle-right%
</div>
@@ -24,9 +24,9 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.root.folder
+root(isDark)
display block
- color #777
+ color isDark ? #fff : #777
text-decoration none !important
*
@@ -55,4 +55,10 @@ export default Vue.extend({
> *
height 100%
+.jvwxssxsytqlqvrpiymarjlzlsxskqsr[data-darkmode]
+ root(true)
+
+.jvwxssxsytqlqvrpiymarjlzlsxskqsr:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue
index c313d225e4..36a6ea2f40 100644
--- a/src/client/app/mobile/views/components/drive.vue
+++ b/src/client/app/mobile/views/components/drive.vue
@@ -1,5 +1,5 @@
<template>
-<div class="mk-drive">
+<div class="kmmwchoexgckptowjmjgfsygeltxfeqs">
<nav ref="nav">
<a @click.prevent="goRoot()" href="/i/drive">%fa:cloud%%i18n:@drive%</a>
<template v-for="folder in hierarchyFolders">
@@ -26,11 +26,11 @@
</p>
</div>
<div class="folders" v-if="folders.length > 0">
- <x-folder v-for="folder in folders" :key="folder.id" :folder="folder"/>
+ <x-folder class="folder" v-for="folder in folders" :key="folder.id" :folder="folder"/>
<p v-if="moreFolders">%i18n:@load-more%</p>
</div>
<div class="files" v-if="files.length > 0">
- <x-file v-for="file in files" :key="file.id" :file="file"/>
+ <x-file class="file" v-for="file in files" :key="file.id" :file="file"/>
<button class="more" v-if="moreFiles" @click="fetchMoreFiles">
{{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:@load-more%' }}
</button>
@@ -94,6 +94,13 @@ export default Vue.extend({
return this.selectFile;
}
},
+ watch: {
+ top() {
+ if (this.isNaked) {
+ (this.$refs.nav as any).style.top = `${this.top}px`;
+ }
+ }
+ },
mounted() {
this.connection = (this as any).os.streams.driveStream.getConnection();
this.connectionId = (this as any).os.streams.driveStream.use();
@@ -466,8 +473,8 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-.mk-drive
- background #fff
+root(isDark)
+ background isDark ? #282c37 : #fff
> nav
display block
@@ -480,10 +487,10 @@ export default Vue.extend({
overflow auto
white-space nowrap
font-size 0.9em
- color rgba(#000, 0.67)
+ color rgba(isDark ? #fff : #000, 0.67)
-webkit-backdrop-filter blur(12px)
backdrop-filter blur(12px)
- background-color rgba(#fff, 0.75)
+ background-color rgba(isDark ? #313543 : #fff, 0.75)
border-bottom solid 1px rgba(#000, 0.13)
> p
@@ -509,7 +516,7 @@ export default Vue.extend({
opacity 0.5
> .info
- border-bottom solid 1px #eee
+ border-bottom solid 1px isDark ? #1c2023 : #eee
&:empty
display none
@@ -520,15 +527,15 @@ export default Vue.extend({
margin 0 auto
padding 4px 16px
font-size 10px
- color #777
+ color isDark ? #606984 : #777
> .folders
> .folder
- border-bottom solid 1px #eee
+ border-bottom solid 1px isDark ? #1c2023 : #eee
> .files
> .file
- border-bottom solid 1px #eee
+ border-bottom solid 1px isDark ? #1c2023 : #eee
> .more
display block
@@ -584,4 +591,10 @@ export default Vue.extend({
> .file
display none
+.kmmwchoexgckptowjmjgfsygeltxfeqs[data-darkmode]
+ root(true)
+
+.kmmwchoexgckptowjmjgfsygeltxfeqs:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/mobile/views/components/follow-button.vue b/src/client/app/mobile/views/components/follow-button.vue
index 360ee91d4b..ff7260edb5 100644
--- a/src/client/app/mobile/views/components/follow-button.vue
+++ b/src/client/app/mobile/views/components/follow-button.vue
@@ -48,12 +48,14 @@ export default Vue.extend({
onFollow(user) {
if (user.id == this.u.id) {
this.u.isFollowing = user.isFollowing;
+ this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
}
},
onUnfollow(user) {
if (user.id == this.u.id) {
this.u.isFollowing = user.isFollowing;
+ this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
}
},
@@ -66,7 +68,7 @@ export default Vue.extend({
userId: this.u.id
});
} else {
- if (this.u.isLocked && this.u.hasPendingFollowRequestFromYou) {
+ if (this.u.hasPendingFollowRequestFromYou) {
this.u = await (this as any).api('following/requests/cancel', {
userId: this.u.id
});
diff --git a/src/client/app/mobile/views/components/friends-maker.vue b/src/client/app/mobile/views/components/friends-maker.vue
index e0461d2bc2..dbb82f4b18 100644
--- a/src/client/app/mobile/views/components/friends-maker.vue
+++ b/src/client/app/mobile/views/components/friends-maker.vue
@@ -47,7 +47,7 @@ export default Vue.extend({
this.fetch();
},
close() {
- this.$destroy();
+ this.destroyDom();
}
}
});
diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue
index e40069bbe3..652a2ad3a4 100644
--- a/src/client/app/mobile/views/components/media-image.vue
+++ b/src/client/app/mobile/views/components/media-image.vue
@@ -1,5 +1,5 @@
<template>
-<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide" @click="hide = false">
+<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
<div>
<b>%fa:exclamation-triangle% %i18n:@sensitive%</b>
<span>%i18n:@click-to-show%</span>
@@ -19,12 +19,13 @@ export default Vue.extend({
},
raw: {
default: false
- },
- hide: {
- type: Boolean,
- default: true
}
},
+ data() {
+ return {
+ hide: true
+ };
+ }
computed: {
style(): any {
let url = `url(${this.image.thumbnailUrl})`;
@@ -65,7 +66,7 @@ export default Vue.extend({
text-align center
font-size 12px
- > b
+ > *
display block
</style>
diff --git a/src/client/app/mobile/views/components/media-video.vue b/src/client/app/mobile/views/components/media-video.vue
index aea7f41460..1e2c1ea7b0 100644
--- a/src/client/app/mobile/views/components/media-video.vue
+++ b/src/client/app/mobile/views/components/media-video.vue
@@ -15,25 +15,28 @@
</template>
<script lang="ts">
-import Vue from 'vue'
+import Vue from 'vue';
+
export default Vue.extend({
props: {
video: {
type: Object,
required: true
- },
- hide: {
- type: Boolean,
- default: true
}
},
+ data() {
+ return {
+ hide: true
+ };
+ },
computed: {
imageStyle(): any {
return {
'background-image': `url(${this.video.url})`
};
}
- },})
+ }
+});
</script>
<style lang="stylus" scoped>
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index f9996f9da6..68be9f8ac4 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -35,20 +35,26 @@
</div>
</header>
<div class="body">
- <div class="text">
- <span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
- <span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
- <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
- </div>
- <div class="media" v-if="p.media.length > 0">
- <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" :detail="true"/>
- <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
- <div class="map" v-if="p.geo" ref="map"></div>
- <div class="renote" v-if="p.renote">
- <mk-note-preview :note="p.renote"/>
+ <p v-if="p.cw != null" class="cw">
+ <span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="p.cw == null || showContent">
+ <div class="text">
+ <span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
+ <span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
+ <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
+ </div>
+ <div class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files" :raw="true"/>
+ </div>
+ <mk-poll v-if="p.poll" :note="p"/>
+ <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
+ <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
+ <div class="map" v-if="p.geo" ref="map"></div>
+ <div class="renote" v-if="p.renote">
+ <mk-note-preview :note="p.renote"/>
+ </div>
</div>
</div>
<router-link class="time" :to="p | notePage">
@@ -85,6 +91,7 @@ import parse from '../../../../../mfm/parse';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './note.sub.vue';
+import { sum } from '../../../../../prelude/array';
export default Vue.extend({
components: {
@@ -103,6 +110,7 @@ export default Vue.extend({
data() {
return {
+ showContent: false,
conversation: [],
conversationFetching: false,
replies: []
@@ -113,19 +121,20 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.length == 0 &&
this.note.poll == null);
},
+
p(): any {
return this.isRenote ? this.note.renote : this.note;
},
+
reactionsCount(): number {
return this.p.reactionCounts
- ? Object.keys(this.p.reactionCounts)
- .map(key => this.p.reactionCounts[key])
- .reduce((a, b) => a + b)
+ ? sum(Object.values(this.p.reactionCounts))
: 0;
},
+
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
@@ -180,16 +189,19 @@ export default Vue.extend({
this.conversation = conversation.reverse();
});
},
+
reply() {
(this as any).apis.post({
reply: this.p
});
},
+
renote() {
(this as any).apis.post({
renote: this.p
});
},
+
react() {
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
@@ -198,6 +210,7 @@ export default Vue.extend({
big: true
});
},
+
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
@@ -328,44 +341,57 @@ root(isDark)
> .body
padding 8px 0
- > .text
+ > .cw
+ cursor default
display block
margin 0
padding 0
overflow-wrap break-word
- font-size 16px
color isDark ? #fff : #717171
- @media (min-width 500px)
- font-size 24px
+ > .text
+ margin-right 8px
+
+ > .content
+
+ > .text
+ display block
+ margin 0
+ padding 0
+ overflow-wrap break-word
+ font-size 16px
+ color isDark ? #fff : #717171
- > .renote
- margin 8px 0
+ @media (min-width 500px)
+ font-size 24px
- > .mk-note-preview
- padding 16px
- border dashed 1px #c0dac6
- border-radius 8px
+ > .renote
+ margin 8px 0
- > .location
- margin 4px 0
- font-size 12px
- color #ccc
+ > *
+ padding 16px
+ border dashed 1px #c0dac6
+ border-radius 8px
- > .map
- width 100%
- height 200px
+ > .location
+ margin 4px 0
+ font-size 12px
+ color #ccc
- &:empty
- display none
+ > .map
+ width 100%
+ height 200px
- > .mk-url-preview
- margin-top 8px
+ &:empty
+ display none
- > .media
- > img
- display block
- max-width 100%
+ > .mk-url-preview
+ margin-top 8px
+
+ > .files
+ > img
+ display block
+ max-width 100%
> .time
font-size 16px
diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
index 5d56d2d326..4c03593a9e 100644
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -1,10 +1,16 @@
<template>
-<div class="mk-note-preview" :class="{ smart: $store.state.device.postStyle == 'smart' }">
+<div class="yohlumlkhizgfkvvscwfcrcggkotpvry" :class="{ smart: $store.state.device.postStyle == 'smart' }">
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
- <mk-sub-note-content class="text" :note="note"/>
+ <p v-if="note.cw != null" class="cw">
+ <span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="note.cw == null || showContent">
+ <mk-sub-note-content class="text" :note="note"/>
+ </div>
</div>
</div>
</div>
@@ -14,7 +20,18 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['note']
+ props: {
+ note: {
+ type: Object,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ showContent: false
+ };
+ }
});
</script>
@@ -65,16 +82,28 @@ root(isDark)
> .body
- > .text
+ > .cw
cursor default
+ display block
margin 0
padding 0
- color isDark ? #959ba7 : #717171
+ overflow-wrap break-word
+ color isDark ? #fff : #717171
+
+ > .text
+ margin-right 8px
+
+ > .content
+ > .text
+ cursor default
+ margin 0
+ padding 0
+ color isDark ? #959ba7 : #717171
-.mk-note-preview[data-darkmode]
+.yohlumlkhizgfkvvscwfcrcggkotpvry[data-darkmode]
root(true)
-.mk-note-preview:not([data-darkmode])
+.yohlumlkhizgfkvvscwfcrcggkotpvry: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 a68aec40a1..c25f827dad 100644
--- a/src/client/app/mobile/views/components/note.sub.vue
+++ b/src/client/app/mobile/views/components/note.sub.vue
@@ -1,10 +1,16 @@
<template>
-<div class="sub" :class="{ smart: $store.state.device.postStyle == 'smart' }">
+<div class="zlrxdaqttccpwhpaagdmkawtzklsccam" :class="{ smart: $store.state.device.postStyle == 'smart' }">
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
- <mk-sub-note-content class="text" :note="note"/>
+ <p v-if="note.cw != null" class="cw">
+ <span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="note.cw == null || showContent">
+ <mk-sub-note-content class="text" :note="note"/>
+ </div>
</div>
</div>
</div>
@@ -24,6 +30,12 @@ export default Vue.extend({
type: Boolean,
default: true
}
+ },
+
+ data() {
+ return {
+ showContent: false
+ };
}
});
</script>
@@ -77,20 +89,31 @@ root(isDark)
margin-bottom 2px
> .body
-
- > .text
+ > .cw
+ cursor default
+ display block
margin 0
padding 0
- color isDark ? #959ba7 : #717171
+ overflow-wrap break-word
+ color isDark ? #fff : #717171
+
+ > .text
+ margin-right 8px
+
+ > .content
+ > .text
+ margin 0
+ padding 0
+ color isDark ? #959ba7 : #717171
- pre
- max-height 120px
- font-size 80%
+ pre
+ max-height 120px
+ font-size 80%
-.sub[data-darkmode]
+.zlrxdaqttccpwhpaagdmkawtzklsccam[data-darkmode]
root(true)
-.sub:not([data-darkmode])
+.zlrxdaqttccpwhpaagdmkawtzklsccam: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 d0cea135f9..8787b39a93 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -18,7 +18,7 @@
<div class="body">
<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 ? '%i18n:@less%' : '%i18n:@more%' }}</span>
+ <mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
@@ -28,16 +28,14 @@
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.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 class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="map" v-if="p.geo" ref="map"></div>
- <div class="renote" v-if="p.renote">
- <mk-note-preview :note="p.renote"/>
- </div>
+ <div class="renote" v-if="p.renote"><mk-note-preview :note="p.renote"/></div>
</div>
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
</div>
@@ -70,6 +68,7 @@ import parse from '../../../../../mfm/parse';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './note.sub.vue';
+import { sum } from '../../../../../prelude/array';
export default Vue.extend({
components: {
@@ -90,7 +89,7 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.length == 0 &&
this.note.poll == null);
},
@@ -100,9 +99,7 @@ export default Vue.extend({
reactionsCount(): number {
return this.p.reactionCounts
- ? Object.keys(this.p.reactionCounts)
- .map(key => this.p.reactionCounts[key])
- .reduce((a, b) => a + b)
+ ? sum(Object.values(this.p.reactionCounts))
: 0;
},
@@ -353,19 +350,6 @@ root(isDark)
> .text
margin-right 8px
- > .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
-
- &:hover
- background isDark ? #707b97 : #bbc4ce
-
> .content
> .text
@@ -414,7 +398,7 @@ root(isDark)
.mk-url-preview
margin-top 8px
- > .media
+ > .files
> img
display block
max-width 100%
@@ -437,7 +421,7 @@ root(isDark)
> .renote
margin 8px 0
- > .mk-note-preview
+ > *
padding 16px
border dashed 1px isDark ? #4e945e : #c0dac6
border-radius 8px
@@ -471,10 +455,6 @@ root(isDark)
&.reacted
color $theme-color
- &.menu
- @media (max-width 350px)
- display none
-
.note[data-darkmode]
root(true)
diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue
index 714e521c0f..401df3ae5b 100644
--- a/src/client/app/mobile/views/components/notes.vue
+++ b/src/client/app/mobile/views/components/notes.vue
@@ -14,8 +14,7 @@
</div>
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!-- <transition-group name="mk-notes" class="transition"> -->
- <div class="transition">
+ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div">
<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">
@@ -23,8 +22,7 @@
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!-- </transition-group> -->
+ </component>
<footer v-if="more">
<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
@@ -125,7 +123,7 @@ export default Vue.extend({
prepend(note, silent = false) {
//#region 弾く
const isMyNote = note.userId == this.$store.state.i.id;
- const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
+ const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
if (this.$store.state.settings.showMyRenotes === false) {
if (isMyNote && isPureRenote) {
diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue
index 9f20c3fb22..11ac23f4b1 100644
--- a/src/client/app/mobile/views/components/notifications.vue
+++ b/src/client/app/mobile/views/components/notifications.vue
@@ -1,8 +1,7 @@
<template>
<div class="mk-notifications">
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!-- <transition-group name="mk-notifications" class="transition notifications"> -->
- <div class="transition notifications">
+ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications">
<template v-for="(notification, i) in _notifications">
<mk-notification :notification="notification" :key="notification.id"/>
<p class="date" :key="notification.id + '_date'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date">
@@ -10,8 +9,7 @@
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!-- </transition-group> -->
+ </component>
<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>
diff --git a/src/client/app/mobile/views/components/notify.vue b/src/client/app/mobile/views/components/notify.vue
index 6d4a481dbe..5f94b91ddd 100644
--- a/src/client/app/mobile/views/components/notify.vue
+++ b/src/client/app/mobile/views/components/notify.vue
@@ -1,6 +1,8 @@
<template>
-<div class="mk-notify">
- <mk-notification-preview :notification="notification"/>
+<div class="mk-notify" :class="pos">
+ <div>
+ <mk-notification-preview :notification="notification"/>
+ </div>
</div>
</template>
@@ -10,11 +12,16 @@ import * as anime from 'animejs';
export default Vue.extend({
props: ['notification'],
+ computed: {
+ pos() {
+ return this.$store.state.device.mobileNotificationPosition;
+ }
+ },
mounted() {
this.$nextTick(() => {
anime({
targets: this.$el,
- bottom: '0px',
+ [this.pos]: '0px',
duration: 500,
easing: 'easeOutQuad'
});
@@ -22,10 +29,10 @@ export default Vue.extend({
setTimeout(() => {
anime({
targets: this.$el,
- bottom: '-64px',
+ [this.pos]: `-${this.$el.offsetHeight}px`,
duration: 500,
easing: 'easeOutQuad',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}, 6000);
});
@@ -35,15 +42,32 @@ export default Vue.extend({
<style lang="stylus" scoped>
.mk-notify
+ $height = 78px
+
position fixed
- z-index 1024
- bottom -64px
+ z-index 10000
left 0
+ right 0
width 100%
- height 64px
+ max-width 500px
+ height $height
+ margin 0 auto
+ padding 8px
pointer-events none
- -webkit-backdrop-filter blur(2px)
- backdrop-filter blur(2px)
- background-color rgba(#000, 0.5)
+ font-size 80%
+
+ &.bottom
+ bottom -($height)
+
+ &.top
+ top -($height)
+
+ > div
+ height 100%
+ -webkit-backdrop-filter blur(2px)
+ backdrop-filter blur(2px)
+ background-color rgba(#000, 0.5)
+ border-radius 7px
+ overflow hidden
</style>
diff --git a/src/client/app/mobile/views/components/post-form-dialog.vue b/src/client/app/mobile/views/components/post-form-dialog.vue
new file mode 100644
index 0000000000..15b36db945
--- /dev/null
+++ b/src/client/app/mobile/views/components/post-form-dialog.vue
@@ -0,0 +1,126 @@
+<template>
+<div class="ulveipglmagnxfgvitaxyszerjwiqmwl">
+ <div class="bg" ref="bg"></div>
+ <div class="main" ref="main">
+ <mk-post-form ref="form"
+ :reply="reply"
+ :renote="renote"
+ :initial-text="initialText"
+ :instant="instant"
+ @posted="onPosted"
+ @cancel="onCanceled"/>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+
+export default Vue.extend({
+ props: {
+ reply: {
+ type: Object,
+ required: false
+ },
+ renote: {
+ type: Object,
+ required: false
+ },
+ initialText: {
+ type: String,
+ required: false
+ },
+ instant: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
+
+ mounted() {
+ this.$nextTick(() => {
+ (this.$refs.bg as any).style.pointerEvents = 'auto';
+ anime({
+ targets: this.$refs.bg,
+ opacity: 1,
+ duration: 100,
+ easing: 'linear'
+ });
+
+ anime({
+ targets: this.$refs.main,
+ opacity: 1,
+ translateY: [-16, 0],
+ duration: 300,
+ easing: 'easeOutQuad'
+ });
+ });
+ },
+
+ methods: {
+ focus() {
+ this.$refs.form.focus();
+ },
+
+ close() {
+ (this.$refs.bg as any).style.pointerEvents = 'none';
+ anime({
+ targets: this.$refs.bg,
+ opacity: 0,
+ duration: 300,
+ easing: 'linear'
+ });
+
+ (this.$refs.main as any).style.pointerEvents = 'none';
+ anime({
+ targets: this.$refs.main,
+ opacity: 0,
+ translateY: 16,
+ duration: 300,
+ easing: 'easeOutQuad',
+ complete: () => this.destroyDom()
+ });
+ },
+
+ onPosted() {
+ this.$emit('posted');
+ this.close();
+ },
+
+ onCanceled() {
+ this.$emit('cancel');
+ this.close();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.ulveipglmagnxfgvitaxyszerjwiqmwl
+ > .bg
+ display block
+ position fixed
+ z-index 10000
+ top 0
+ left 0
+ width 100%
+ height 100%
+ background rgba(#000, 0.7)
+ opacity 0
+ pointer-events none
+
+ > .main
+ display block
+ position fixed
+ z-index 10000
+ top 0
+ left 0
+ right 0
+ height 100%
+ overflow auto
+ margin 0 auto 0 auto
+ opacity 0
+ transform translateY(-16px)
+
+</style>
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index a74df67c0a..1294273a2a 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -4,14 +4,14 @@
<header>
<button class="cancel" @click="cancel">%fa:times%</button>
<div>
- <span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span>
+ <span class="text-count" :class="{ over: trimmedLength(text) > 1000 }">{{ 1000 - trimmedLength(text) }}</span>
<span class="geo" v-if="geo">%fa:map-marker-alt%</span>
<button class="submit" :disabled="!canPost" @click="post">{{ submitText }}</button>
</div>
</header>
<div class="form">
- <mk-note-preview v-if="reply" :note="reply"/>
- <mk-note-preview v-if="renote" :note="renote"/>
+ <mk-note-preview class="preview" v-if="reply" :note="reply"/>
+ <mk-note-preview class="preview" v-if="renote" :note="renote"/>
<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">+%i18n:@add-visible-user%</a>
@@ -42,7 +42,7 @@
<span v-if="visibility === 'private'">%fa:lock%</span>
</button>
</footer>
- <input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
+ <input ref="file" class="file" type="file" multiple="multiple" @change="onChangeFile"/>
</div>
</div>
<div class="hashtags" v-if="recentHashtags.length > 0 && $store.state.settings.suggestRecentHashtags">
@@ -59,6 +59,9 @@ import MkVisibilityChooser from '../../../common/views/components/visibility-cho
import getFace from '../../../common/scripts/get-face';
import parse from '../../../../../mfm/parse';
import { host } from '../../../config';
+import { erase, unique } from '../../../../../prelude/array';
+import { length } from 'stringz';
+import parseAcct from '../../../../../misc/acct/parse';
export default Vue.extend({
components: {
@@ -94,7 +97,7 @@ export default Vue.extend({
files: [],
poll: false,
geo: null,
- visibility: this.$store.state.device.visibility || 'public',
+ visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility,
visibleUsers: [],
useCw: false,
cw: null,
@@ -105,9 +108,9 @@ export default Vue.extend({
computed: {
draftId(): string {
return this.renote
- ? 'renote:' + this.renote.id
+ ? `renote:${this.renote.id}`
: this.reply
- ? 'reply:' + this.reply.id
+ ? `reply:${this.reply.id}`
: 'note';
},
@@ -170,12 +173,18 @@ export default Vue.extend({
});
}
+ this.focus();
+
this.$nextTick(() => {
this.focus();
});
},
methods: {
+ trimmedLength(text: string) {
+ return length(text.trim());
+ },
+
addTag(tag: string) {
insertTextAtCursor(this.$refs.text, ` #${tag} `);
},
@@ -198,12 +207,12 @@ export default Vue.extend({
attachMedia(driveFile) {
this.files.push(driveFile);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
detachMedia(file) {
this.files = this.files.filter(x => x.id != file.id);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
onChangeFile() {
@@ -227,7 +236,7 @@ export default Vue.extend({
navigator.geolocation.getCurrentPosition(pos => {
this.geo = pos.coords;
}, err => {
- alert('%i18n:@error%: ' + err.message);
+ alert(`%i18n:@error%: ${err.message}`);
}, {
enableHighAccuracy: true
});
@@ -250,24 +259,23 @@ export default Vue.extend({
addVisibleUser() {
(this as any).apis.input({
title: '%i18n:@username-prompt%'
- }).then(username => {
- (this as any).api('users/show', {
- username
- }).then(user => {
+ }).then(acct => {
+ if (acct.startsWith('@')) acct = acct.substr(1);
+ (this as any).api('users/show', parseAcct(acct)).then(user => {
this.visibleUsers.push(user);
});
});
},
removeVisibleUser(user) {
- this.visibleUsers = this.visibleUsers.filter(u => u != user);
+ this.visibleUsers = erase(user, this.visibleUsers);
},
clear() {
this.text = '';
this.files = [];
this.poll = false;
- this.$emit('change-attached-media');
+ this.$emit('change-attached-files');
},
post() {
@@ -275,7 +283,7 @@ export default Vue.extend({
const viaMobile = this.$store.state.settings.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,
+ fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined,
renoteId: this.renote ? this.renote.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
@@ -293,9 +301,6 @@ export default Vue.extend({
viaMobile: viaMobile
}).then(data => {
this.$emit('posted');
- this.$nextTick(() => {
- this.$destroy();
- });
}).catch(err => {
this.posting = false;
});
@@ -303,13 +308,12 @@ export default Vue.extend({
if (this.text && this.text != '') {
const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
- localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], [])));
+ localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}
},
cancel() {
this.$emit('cancel');
- this.$destroy();
},
kao() {
@@ -383,7 +387,7 @@ root(isDark)
max-width 500px
margin 0 auto
- > .mk-note-preview
+ > .preview
padding 16px
> .visibleUsers
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 a4ce49786e..4d0aa25f34 100644
--- a/src/client/app/mobile/views/components/sub-note-content.vue
+++ b/src/client/app/mobile/views/components/sub-note-content.vue
@@ -7,9 +7,9 @@
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
<a class="rp" v-if="note.renoteId">RP: ...</a>
</div>
- <details v-if="note.media.length > 0">
- <summary>({{ '%i18n:@media-count%'.replace('{}', note.media.length) }})</summary>
- <mk-media-list :media-list="note.media"/>
+ <details v-if="note.files.length > 0">
+ <summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary>
+ <mk-media-list :media-list="note.files"/>
</details>
<details v-if="note.poll">
<summary>%i18n:@poll%</summary>
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
index a616586c56..c9b3ab51ae 100644
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ b/src/client/app/mobile/views/components/ui.header.vue
@@ -1,5 +1,6 @@
<template>
-<div class="header">
+<div class="header" ref="root">
+ <p class="warn" v-if="env != 'production'">%i18n:common.do-not-use-in-production%</p>
<mk-special-message/>
<div class="main" ref="main">
<div class="backdrop"></div>
@@ -20,6 +21,7 @@
<script lang="ts">
import Vue from 'vue';
import * as anime from 'animejs';
+import { env } from '../../../config';
export default Vue.extend({
props: ['func'],
@@ -27,7 +29,8 @@ export default Vue.extend({
return {
hasGameInvitation: false,
connection: null,
- connectionId: null
+ connectionId: null,
+ env: env
};
},
computed: {
@@ -39,7 +42,7 @@ export default Vue.extend({
}
},
mounted() {
- this.$store.commit('setUiHeaderHeight', 48);
+ this.$store.commit('setUiHeaderHeight', this.$refs.root.offsetHeight);
if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection();
@@ -133,6 +136,15 @@ root(isDark)
height 3px
background $theme-color
+ > .warn
+ display block
+ margin 0
+ padding 4px
+ text-align center
+ font-size 12px
+ background #f00
+ color #fff
+
> .main
color rgba(#fff, 0.9)
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index 39ea513b76..c3ae05fef6 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -34,6 +34,12 @@
<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li>
</ul>
</div>
+ <div class="announcements" v-if="announcements && announcements.length > 0">
+ <article v-for="announcement in announcements">
+ <span v-html="announcement.title" class="title"></span>
+ <div v-html="announcement.text"></div>
+ </article>
+ </div>
<a :href="aboutUrl"><p class="about">%i18n:@about%</p></a>
</div>
</transition>
@@ -46,23 +52,32 @@ import { lang } from '../../../config';
export default Vue.extend({
props: ['isOpen'],
+
data() {
return {
hasGameInvitation: false,
connection: null,
connectionId: null,
- aboutUrl: `/docs/${lang}/about`
+ aboutUrl: `/docs/${lang}/about`,
+ announcements: []
};
},
+
computed: {
hasUnreadNotification(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
},
+
hasUnreadMessagingMessage(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
}
},
+
mounted() {
+ (this as any).os.getMeta().then(meta => {
+ this.announcements = meta.broadcasts;
+ });
+
if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
@@ -71,6 +86,7 @@ export default Vue.extend({
this.connection.on('reversi_no_invites', this.onReversiNoInvites);
}
},
+
beforeDestroy() {
if (this.$store.getters.isSignedIn) {
this.connection.off('reversi_invited', this.onReversiInvited);
@@ -78,18 +94,22 @@ export default Vue.extend({
(this as any).os.stream.dispose(this.connectionId);
}
},
+
methods: {
search() {
const query = window.prompt('%i18n:@search%');
if (query == null || query == '') return;
- this.$router.push('/search?q=' + encodeURIComponent(query));
+ this.$router.push(`/search?q=${encodeURIComponent(query)}`);
},
+
onReversiInvited() {
this.hasGameInvitation = true;
},
+
onReversiNoInvites() {
this.hasGameInvitation = false;
},
+
dark() {
this.$store.commit('device/set', {
key: 'darkmode',
@@ -204,6 +224,17 @@ root(isDark)
color $color
opacity 0.5
+ .announcements
+ > article
+ background isDark ? rgba(30, 129, 216, 0.2) : rgba(155, 196, 232, 0.2)
+ color isDark ? #fff : #3f4967
+ padding 16px
+ margin 8px 0
+ font-size 12px
+
+ > .title
+ font-weight bold
+
.about
margin 0 0 8px 0
padding 1em 0
diff --git a/src/client/app/mobile/views/components/ui.vue b/src/client/app/mobile/views/components/ui.vue
index 7e2d39f259..d2af15d235 100644
--- a/src/client/app/mobile/views/components/ui.vue
+++ b/src/client/app/mobile/views/components/ui.vue
@@ -31,7 +31,14 @@ export default Vue.extend({
connectionId: null
};
},
+ watch: {
+ '$store.state.uiHeaderHeight'() {
+ this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
+ }
+ },
mounted() {
+ this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
+
if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue
index 6be675c0a7..7cd23d6655 100644
--- a/src/client/app/mobile/views/components/user-timeline.vue
+++ b/src/client/app/mobile/views/components/user-timeline.vue
@@ -41,7 +41,7 @@ export default Vue.extend({
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api('users/notes', {
userId: this.user.id,
- withMedia: this.withMedia,
+ withFiles: this.withMedia,
limit: fetchLimit + 1
}).then(notes => {
if (notes.length == fetchLimit + 1) {
@@ -62,7 +62,7 @@ export default Vue.extend({
const promise = (this as any).api('users/notes', {
userId: this.user.id,
- withMedia: this.withMedia,
+ withFiles: this.withMedia,
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id
});
diff --git a/src/client/app/mobile/views/pages/drive.vue b/src/client/app/mobile/views/pages/drive.vue
index c7cbe0f72e..27ac956043 100644
--- a/src/client/app/mobile/views/pages/drive.vue
+++ b/src/client/app/mobile/views/pages/drive.vue
@@ -11,7 +11,7 @@
:init-folder="initFolder"
:init-file="initFile"
:is-naked="true"
- :top="48"
+ :top="$store.state.uiHeaderHeight"
@begin-fetch="Progress.start()"
@fetched-mid="Progress.set(0.5)"
@fetched="Progress.done()"
@@ -80,7 +80,7 @@ export default Vue.extend({
if (!silent) {
// Rewrite URL
- history.pushState(null, title, '/i/drive/folder/' + folder.id);
+ history.pushState(null, title, `/i/drive/folder/${folder.id}`);
}
document.title = title;
@@ -93,7 +93,7 @@ export default Vue.extend({
if (!silent) {
// Rewrite URL
- history.pushState(null, title, '/i/drive/file/' + file.id);
+ history.pushState(null, title, `/i/drive/file/${file.id}`);
}
document.title = title;
diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue
index 421c150856..601f6670c1 100644
--- a/src/client/app/mobile/views/pages/followers.vue
+++ b/src/client/app/mobile/views/pages/followers.vue
@@ -49,7 +49,7 @@ export default Vue.extend({
this.user = user;
this.fetching = false;
- document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | ' + (this as any).os.instanceName;
+ document.title = `${'%i18n:@followers-of%'.replace('{}', this.name)} | ${(this as any).os.instanceName}`;
});
},
onLoaded() {
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
index ff201ff2bd..0efac6110e 100644
--- a/src/client/app/mobile/views/pages/following.vue
+++ b/src/client/app/mobile/views/pages/following.vue
@@ -48,7 +48,7 @@ export default Vue.extend({
this.user = user;
this.fetching = false;
- document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | ' + (this as any).os.instanceName;
+ document.title = `${'%i18n:@followers-of%'.replace('{}', this.name)} | ${(this as any).os.instanceName}`;
});
},
onLoaded() {
diff --git a/src/client/app/mobile/views/pages/games/reversi.vue b/src/client/app/mobile/views/pages/games/reversi.vue
index d6849a1c11..bdadc88a43 100644
--- a/src/client/app/mobile/views/pages/games/reversi.vue
+++ b/src/client/app/mobile/views/pages/games/reversi.vue
@@ -16,10 +16,10 @@ export default Vue.extend({
methods: {
nav(game, actualNav) {
if (actualNav) {
- this.$router.push('/reversi/' + game.id);
+ this.$router.push(`/reversi/${game.id}`);
} else {
// TODO: https://github.com/vuejs/vue-router/issues/703
- this.$router.push('/reversi/' + game.id);
+ this.$router.push(`/reversi/${game.id}`);
}
}
}
diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue
index 416b006cd8..fecb2384ba 100644
--- a/src/client/app/mobile/views/pages/home.timeline.vue
+++ b/src/client/app/mobile/views/pages/home.timeline.vue
@@ -13,6 +13,7 @@
<script lang="ts">
import Vue from 'vue';
+import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
const fetchLimit = 10;
@@ -21,6 +22,9 @@ export default Vue.extend({
src: {
type: String,
required: true
+ },
+ tagTl: {
+ required: false
}
},
@@ -29,6 +33,7 @@ export default Vue.extend({
fetching: true,
moreFetching: false,
existMore: false,
+ streamManager: null,
connection: null,
connectionId: null,
unreadCount: 0,
@@ -41,21 +46,14 @@ export default Vue.extend({
return this.$store.state.i.followingCount == 0;
},
- stream(): any {
- switch (this.src) {
- case 'home': return (this as any).os.stream;
- case 'local': return (this as any).os.streams.localTimelineStream;
- case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
- case 'global': return (this as any).os.streams.globalTimelineStream;
- }
- },
-
endpoint(): string {
switch (this.src) {
case 'home': return 'notes/timeline';
case 'local': return 'notes/local-timeline';
case 'hybrid': return 'notes/hybrid-timeline';
case 'global': return 'notes/global-timeline';
+ case 'mentions': return 'notes/mentions';
+ case 'tag': return 'notes/search_by_tag';
}
},
@@ -65,25 +63,63 @@ export default Vue.extend({
},
mounted() {
- this.connection = this.stream.getConnection();
- this.connectionId = this.stream.use();
-
- this.connection.on('note', this.onNote);
- if (this.src == 'home') {
+ if (this.src == 'tag') {
+ this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
+ this.connection.on('note', this.onNote);
+ } else if (this.src == 'home') {
+ this.streamManager = (this as any).os.stream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', this.onNote);
this.connection.on('follow', this.onChangeFollowing);
this.connection.on('unfollow', this.onChangeFollowing);
+ } else if (this.src == 'local') {
+ this.streamManager = (this as any).os.streams.localTimelineStream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', this.onNote);
+ } else if (this.src == 'hybrid') {
+ this.streamManager = (this as any).os.streams.hybridTimelineStream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', this.onNote);
+ } else if (this.src == 'global') {
+ this.streamManager = (this as any).os.streams.globalTimelineStream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', this.onNote);
+ } else if (this.src == 'mentions') {
+ this.streamManager = (this as any).os.stream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('mention', this.onNote);
}
this.fetch();
},
beforeDestroy() {
- this.connection.off('note', this.onNote);
- if (this.src == 'home') {
+ if (this.src == 'tag') {
+ this.connection.off('note', this.onNote);
+ this.connection.close();
+ } else if (this.src == 'home') {
+ this.connection.off('note', this.onNote);
this.connection.off('follow', this.onChangeFollowing);
this.connection.off('unfollow', this.onChangeFollowing);
+ this.streamManager.dispose(this.connectionId);
+ } else if (this.src == 'local') {
+ this.connection.off('note', this.onNote);
+ this.streamManager.dispose(this.connectionId);
+ } else if (this.src == 'hybrid') {
+ this.connection.off('note', this.onNote);
+ this.streamManager.dispose(this.connectionId);
+ } else if (this.src == 'global') {
+ this.connection.off('note', this.onNote);
+ this.streamManager.dispose(this.connectionId);
+ } else if (this.src == 'mentions') {
+ this.connection.off('mention', this.onNote);
+ this.streamManager.dispose(this.connectionId);
}
- this.stream.dispose(this.connectionId);
},
methods: {
@@ -96,7 +132,8 @@ export default Vue.extend({
untilDate: this.date ? this.date.getTime() : undefined,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
+ query: this.tagTl ? this.tagTl.query : undefined
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
@@ -119,7 +156,8 @@ export default Vue.extend({
untilId: (this.$refs.timeline as any).tail().id,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
+ query: this.tagTl ? this.tagTl.query : undefined
});
promise.then(notes => {
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index 706c9cd28b..3ec2f16b75 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -6,7 +6,9 @@
<span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span>
<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span>
<span v-if="src == 'global'">%fa:globe%%i18n:@global%</span>
+ <span v-if="src == 'mentions'">%fa:at%%i18n:@mentions%</span>
<span v-if="src == 'list'">%fa:list%{{ list.title }}</span>
+ <span v-if="src == 'tag'">%fa:hashtag%{{ tagTl.title }}</span>
</span>
<span style="margin-left:8px">
<template v-if="!showNav">%fa:angle-down%</template>
@@ -24,12 +26,14 @@
<div class="body">
<div>
<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 == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span>
+ <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
+ <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
+ <span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span>
<template v-if="lists">
<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
</template>
+ <span v-for="tl in $store.state.settings.tagTimelines" :data-active="src == 'tag' && tagTl == tl" @click="src = 'tag'; tagTl = tl" :key="tl.id">%fa:hashtag% {{ tl.title }}</span>
</div>
</div>
</div>
@@ -39,6 +43,8 @@
<x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/>
<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
+ <x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
+ <x-tl v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/>
<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
</div>
</main>
@@ -60,7 +66,9 @@ export default Vue.extend({
src: 'home',
list: null,
lists: null,
- showNav: false
+ tagTl: null,
+ showNav: false,
+ enableLocalTimeline: false
};
},
@@ -70,9 +78,16 @@ export default Vue.extend({
this.saveSrc();
},
- list() {
+ list(x) {
this.showNav = false;
this.saveSrc();
+ if (x != null) this.tagTl = null;
+ },
+
+ tagTl(x) {
+ this.showNav = false;
+ this.saveSrc();
+ if (x != null) this.list = null;
},
showNav(v) {
@@ -85,10 +100,16 @@ export default Vue.extend({
},
created() {
+ (this as any).os.getMeta().then(meta => {
+ this.enableLocalTimeline = !meta.disableLocalTimeline;
+ });
+
if (this.$store.state.device.tl) {
this.src = this.$store.state.device.tl.src;
if (this.src == 'list') {
this.list = this.$store.state.device.tl.arg;
+ } else if (this.src == 'tag') {
+ this.tagTl = this.$store.state.device.tl.arg;
}
} else if (this.$store.state.i.followingCount == 0) {
this.src = 'hybrid';
@@ -113,7 +134,7 @@ export default Vue.extend({
saveSrc() {
this.$store.commit('device/setTl', {
src: this.src,
- arg: this.list
+ arg: this.src == 'list' ? this.list : this.tagTl
});
},
diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue
index 1a162b346c..c098b8c65e 100644
--- a/src/client/app/mobile/views/pages/selectdrive.vue
+++ b/src/client/app/mobile/views/pages/selectdrive.vue
@@ -5,7 +5,7 @@
<button class="upload" @click="upload">%fa:upload%</button>
<button v-if="multiple" class="ok" @click="ok">%fa:check%</button>
</header>
- <mk-drive ref="browser" select-file :multiple="multiple" is-naked :top="42"/>
+ <mk-drive ref="browser" select-file :multiple="multiple" is-naked :top="$store.state.uiHeaderHeight"/>
</div>
</template>
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index 7437eb8b47..f315c058df 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -2,7 +2,7 @@
<mk-ui>
<span slot="header">%fa:cog%%i18n:@settings%</span>
<main :data-darkmode="$store.state.device.darkmode">
- <div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></div>
+ <div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${name}</b>`)"></div>
<div>
<x-profile/>
@@ -10,80 +10,120 @@
<ui-card>
<div slot="title">%fa:palette% %i18n:@design%</div>
- <ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
- <ui-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</ui-switch>
- <ui-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi">%i18n:common.i-like-sushi%</ui-switch>
- <ui-switch v-model="$store.state.settings.disableAnimatedMfm" @change="onChangeDisableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch>
- <ui-switch v-model="$store.state.settings.games.reversi.showBoardLabels" @change="onChangeReversiBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch>
- <ui-switch v-model="$store.state.settings.games.reversi.useContrastStones" @change="onChangeUseContrastReversiStones">%i18n:common.use-contrast-reversi-stones%</ui-switch>
+ <section>
+ <ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
+ <ui-switch v-model="circleIcons">%i18n:@circle-icons%</ui-switch>
+ <ui-switch v-model="reduceMotion">%i18n:common.reduce-motion% (%i18n:common.this-setting-is-this-device-only%)</ui-switch>
+ <ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch>
+ <ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch>
+ <ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch>
+ <ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch>
+ <ui-switch v-model="alwaysShowNsfw">%i18n:common.always-show-nsfw% (%i18n:common.this-setting-is-this-device-only%)</ui-switch>
+ <ui-switch v-model="games_reversi_showBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch>
+ <ui-switch v-model="games_reversi_useContrastStones">%i18n:common.use-contrast-reversi-stones%</ui-switch>
+ </section>
- <div>
- <div>%i18n:@timeline%</div>
- <ui-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</ui-switch>
- <ui-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</ui-switch>
- <ui-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch>
- <ui-switch v-model="$store.state.settings.showLocalRenotes" @change="onChangeShowLocalRenotes">%i18n:@show-local-renotes%</ui-switch>
- </div>
+ <section>
+ <header>%i18n:@timeline%</header>
+ <div>
+ <ui-switch v-model="showReplyTarget">%i18n:@show-reply-target%</ui-switch>
+ <ui-switch v-model="showMyRenotes">%i18n:@show-my-renotes%</ui-switch>
+ <ui-switch v-model="showRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch>
+ <ui-switch v-model="showLocalRenotes">%i18n:@show-local-renotes%</ui-switch>
+ </div>
+ </section>
- <div>
- <div>%i18n:@post-style%</div>
+ <section>
+ <header>%i18n:@post-style%</header>
<ui-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</ui-radio>
<ui-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</ui-radio>
- </div>
+ </section>
+
+ <section>
+ <header>%i18n:@notification-position%</header>
+ <ui-radio v-model="mobileNotificationPosition" value="bottom">%i18n:@notification-position-bottom%</ui-radio>
+ <ui-radio v-model="mobileNotificationPosition" value="top">%i18n:@notification-position-top%</ui-radio>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:cog% %i18n:@behavior%</div>
- <ui-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</ui-switch>
- <ui-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</ui-switch>
- <ui-switch v-model="loadRawImages">%i18n:@load-raw-images%</ui-switch>
- <ui-switch v-model="$store.state.settings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</ui-switch>
- <ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
+
+ <section>
+ <ui-switch v-model="fetchOnScroll">%i18n:@fetch-on-scroll%</ui-switch>
+ <ui-switch v-model="disableViaMobile">%i18n:@disable-via-mobile%</ui-switch>
+ <ui-switch v-model="loadRawImages">%i18n:@load-raw-images%</ui-switch>
+ <ui-switch v-model="loadRemoteMedia">%i18n:@load-remote-media%</ui-switch>
+ <ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
+ </section>
+
+ <section>
+ <header>%i18n:@note-visibility%</header>
+ <ui-switch v-model="rememberNoteVisibility">%i18n:@remember-note-visibility%</ui-switch>
+ <section>
+ <header>%i18n:@default-note-visibility%</header>
+ <ui-select v-model="defaultNoteVisibility">
+ <option value="public">%i18n:common.note-visibility.public%</option>
+ <option value="home">%i18n:common.note-visibility.home%</option>
+ <option value="followers">%i18n:common.note-visibility.followers%</option>
+ <option value="specified">%i18n:common.note-visibility.specified%</option>
+ <option value="private">%i18n:common.note-visibility.private%</option>
+ </ui-select>
+ </section>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:volume-up% %i18n:@sound%</div>
- <ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch>
+ <section>
+ <ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:language% %i18n:@lang%</div>
- <ui-select v-model="lang" placeholder="%i18n:@auto%">
- <optgroup label="%i18n:@recommended%">
- <option value="">%i18n:@auto%</option>
- </optgroup>
+ <section class="fit-top">
+ <ui-select v-model="lang" placeholder="%i18n:@auto%">
+ <optgroup label="%i18n:@recommended%">
+ <option value="">%i18n:@auto%</option>
+ </optgroup>
- <optgroup label="%i18n:@specify-language%">
- <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
- </optgroup>
- </ui-select>
- <span>%fa:info-circle% %i18n:@lang-tip%</span>
+ <optgroup label="%i18n:@specify-language%">
+ <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
+ </optgroup>
+ </ui-select>
+ <span>%fa:info-circle% %i18n:@lang-tip%</span>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:B twitter% %i18n:@twitter%</div>
- <p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
- <p>
- <a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
- <span v-if="$store.state.i.twitter"> or </span>
- <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a>
- </p>
+ <section>
+ <p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
+ <p>
+ <a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
+ <span v-if="$store.state.i.twitter"> or </span>
+ <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a>
+ </p>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:sync-alt% %i18n:@update%</div>
- <div>%i18n:@version% <i>{{ version }}</i></div>
- <template v-if="latestVersion !== undefined">
- <div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div>
- </template>
- <ui-button @click="checkForUpdate" :disabled="checkingForUpdate">
- <template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
- <template v-else>%i18n:@check-for-updates%</template>
- </ui-button>
+ <section>
+ <div>%i18n:@version% <i>{{ version }}</i></div>
+ <template v-if="latestVersion !== undefined">
+ <div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div>
+ </template>
+ <ui-button @click="checkForUpdate" :disabled="checkingForUpdate">
+ <template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
+ <template v-else>%i18n:@check-for-updates%</template>
+ </ui-button>
+ </section>
</ui-card>
</div>
@@ -129,11 +169,26 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
},
+ reduceMotion: {
+ get() { return this.$store.state.device.reduceMotion; },
+ set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
+ },
+
+ alwaysShowNsfw: {
+ get() { return this.$store.state.device.alwaysShowNsfw; },
+ set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); }
+ },
+
postStyle: {
get() { return this.$store.state.device.postStyle; },
set(value) { this.$store.commit('device/set', { key: 'postStyle', value }); }
},
+ mobileNotificationPosition: {
+ get() { return this.$store.state.device.mobileNotificationPosition; },
+ set(value) { this.$store.commit('device/set', { key: 'mobileNotificationPosition', value }); }
+ },
+
lightmode: {
get() { return this.$store.state.device.lightmode; },
set(value) { this.$store.commit('device/set', { key: 'lightmode', value }); }
@@ -153,99 +208,95 @@ export default Vue.extend({
get() { return this.$store.state.device.enableSounds; },
set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }
},
- },
- mounted() {
- document.title = '%i18n:@settings%';
- },
+ fetchOnScroll: {
+ get() { return this.$store.state.settings.fetchOnScroll; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); }
+ },
- methods: {
- signout() {
- (this as any).os.signout();
+ rememberNoteVisibility: {
+ get() { return this.$store.state.settings.rememberNoteVisibility; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
},
- onChangeFetchOnScroll(v) {
- this.$store.dispatch('settings/set', {
- key: 'fetchOnScroll',
- value: v
- });
+ disableViaMobile: {
+ get() { return this.$store.state.settings.disableViaMobile; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'disableViaMobile', value }); }
},
- onChangeDisableViaMobile(v) {
- this.$store.dispatch('settings/set', {
- key: 'disableViaMobile',
- value: v
- });
+ loadRemoteMedia: {
+ get() { return this.$store.state.settings.loadRemoteMedia; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'loadRemoteMedia', value }); }
},
- onChangeLoadRemoteMedia(v) {
- this.$store.dispatch('settings/set', {
- key: 'loadRemoteMedia',
- value: v
- });
+ circleIcons: {
+ get() { return this.$store.state.settings.circleIcons; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'circleIcons', value }); }
},
- onChangeCircleIcons(v) {
- this.$store.dispatch('settings/set', {
- key: 'circleIcons',
- value: v
- });
+ contrastedAcct: {
+ get() { return this.$store.state.settings.contrastedAcct; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'contrastedAcct', value }); }
},
- onChangeILikeSushi(v) {
- this.$store.dispatch('settings/set', {
- key: 'iLikeSushi',
- value: v
- });
+ showFullAcct: {
+ get() { return this.$store.state.settings.showFullAcct; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showFullAcct', value }); }
},
- onChangeReversiBoardLabels(v) {
- this.$store.dispatch('settings/set', {
- key: 'games.reversi.showBoardLabels',
- value: v
- });
+ iLikeSushi: {
+ get() { return this.$store.state.settings.iLikeSushi; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'iLikeSushi', value }); }
},
- onChangeUseContrastReversiStones(v) {
- this.$store.dispatch('settings/set', {
- key: 'games.reversi.useContrastStones',
- value: v
- });
+ games_reversi_showBoardLabels: {
+ get() { return this.$store.state.settings.games.reversi.showBoardLabels; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.showBoardLabels', value }); }
},
- onChangeDisableAnimatedMfm(v) {
- this.$store.dispatch('settings/set', {
- key: 'disableAnimatedMfm',
- value: v
- });
+ games_reversi_useContrastStones: {
+ get() { return this.$store.state.settings.games.reversi.useContrastStones; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.useContrastStones', value }); }
},
- onChangeShowReplyTarget(v) {
- this.$store.dispatch('settings/set', {
- key: 'showReplyTarget',
- value: v
- });
+ disableAnimatedMfm: {
+ get() { return this.$store.state.settings.disableAnimatedMfm; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); }
},
- onChangeShowMyRenotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showMyRenotes',
- value: v
- });
+ showReplyTarget: {
+ get() { return this.$store.state.settings.showReplyTarget; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); }
},
- onChangeShowRenotedMyNotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showRenotedMyNotes',
- value: v
- });
+ showMyRenotes: {
+ get() { return this.$store.state.settings.showMyRenotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showMyRenotes', value }); }
},
- onChangeShowLocalRenotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showLocalRenotes',
- value: v
- });
+ showRenotedMyNotes: {
+ get() { return this.$store.state.settings.showRenotedMyNotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showRenotedMyNotes', value }); }
+ },
+
+ showLocalRenotes: {
+ get() { return this.$store.state.settings.showLocalRenotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showLocalRenotes', value }); }
+ },
+
+ defaultNoteVisibility: {
+ get() { return this.$store.state.settings.defaultNoteVisibility; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
+ },
+ },
+
+ mounted() {
+ document.title = '%i18n:@settings%';
+ },
+
+ methods: {
+ signout() {
+ (this as any).os.signout();
},
checkForUpdate() {
@@ -273,7 +324,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
root(isDark)
margin 0 auto
- max-width 500px
+ max-width 600px
width 100%
> .signin-as
diff --git a/src/client/app/mobile/views/pages/settings/settings.profile.vue b/src/client/app/mobile/views/pages/settings/settings.profile.vue
index 3b797cdde1..127f531902 100644
--- a/src/client/app/mobile/views/pages/settings/settings.profile.vue
+++ b/src/client/app/mobile/views/pages/settings/settings.profile.vue
@@ -2,47 +2,64 @@
<ui-card>
<div slot="title">%fa:user% %i18n:@title%</div>
- <ui-form :disabled="saving">
- <ui-input v-model="name" :max="30">
- <span>%i18n:@name%</span>
- </ui-input>
+ <section class="fit-top">
+ <ui-form :disabled="saving">
+ <ui-input v-model="name" :max="30">
+ <span>%i18n:@name%</span>
+ </ui-input>
- <ui-input v-model="username" readonly>
- <span>%i18n:@account%</span>
- <span slot="prefix">@</span>
- <span slot="suffix">@{{ host }}</span>
- </ui-input>
+ <ui-input v-model="username" readonly>
+ <span>%i18n:@account%</span>
+ <span slot="prefix">@</span>
+ <span slot="suffix">@{{ host }}</span>
+ </ui-input>
- <ui-input v-model="location">
- <span>%i18n:@location%</span>
- <span slot="prefix">%fa:map-marker-alt%</span>
- </ui-input>
+ <ui-input v-model="location">
+ <span>%i18n:@location%</span>
+ <span slot="prefix">%fa:map-marker-alt%</span>
+ </ui-input>
- <ui-input v-model="birthday" type="date">
- <span>%i18n:@birthday%</span>
- <span slot="prefix">%fa:birthday-cake%</span>
- </ui-input>
+ <ui-input v-model="birthday" type="date">
+ <span>%i18n:@birthday%</span>
+ <span slot="prefix">%fa:birthday-cake%</span>
+ </ui-input>
- <ui-textarea v-model="description" :max="500">
- <span>%i18n:@description%</span>
- </ui-textarea>
+ <ui-textarea v-model="description" :max="500">
+ <span>%i18n:@description%</span>
+ </ui-textarea>
- <ui-input type="file" @change="onAvatarChange">
- <span>%i18n:@avatar%</span>
- <span slot="icon">%fa:image%</span>
- <span slot="text" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span>
- </ui-input>
+ <ui-input type="file" @change="onAvatarChange">
+ <span>%i18n:@avatar%</span>
+ <span slot="icon">%fa:image%</span>
+ <span slot="text" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span>
+ </ui-input>
- <ui-input type="file" @change="onBannerChange">
- <span>%i18n:@banner%</span>
- <span slot="icon">%fa:image%</span>
- <span slot="text" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span>
- </ui-input>
+ <ui-input type="file" @change="onBannerChange">
+ <span>%i18n:@banner%</span>
+ <span slot="icon">%fa:image%</span>
+ <span slot="text" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span>
+ </ui-input>
- <ui-switch v-model="isCat">%i18n:@is-cat%</ui-switch>
+ <ui-button @click="save(true)">%i18n:@save%</ui-button>
+ </ui-form>
+ </section>
- <ui-button @click="save">%i18n:@save%</ui-button>
- </ui-form>
+ <section>
+ <header>%i18n:@advanced%</header>
+
+ <div>
+ <ui-switch v-model="isCat" @change="save(false)">%i18n:@is-cat%</ui-switch>
+ <ui-switch v-model="alwaysMarkNsfw">%i18n:common.always-mark-nsfw%</ui-switch>
+ </div>
+ </section>
+
+ <section>
+ <header>%i18n:@privacy%</header>
+
+ <div>
+ <ui-switch v-model="isLocked" @change="save(false)">%i18n:@is-locked%</ui-switch>
+ </div>
+ </section>
</ui-card>
</template>
@@ -62,12 +79,20 @@ export default Vue.extend({
avatarId: null,
bannerId: null,
isCat: false,
+ isLocked: false,
saving: false,
avatarUploading: false,
bannerUploading: false
};
},
+ computed: {
+ alwaysMarkNsfw: {
+ get() { return this.$store.state.i.settings.alwaysMarkNsfw; },
+ set(value) { (this as any).api('i/update', { alwaysMarkNsfw: value }); }
+ },
+ },
+
created() {
this.name = this.$store.state.i.name || '';
this.username = this.$store.state.i.username;
@@ -77,6 +102,7 @@ export default Vue.extend({
this.avatarId = this.$store.state.i.avatarId;
this.bannerId = this.$store.state.i.bannerId;
this.isCat = this.$store.state.i.isCat;
+ this.isLocked = this.$store.state.i.isLocked;
},
methods: {
@@ -124,7 +150,7 @@ export default Vue.extend({
});
},
- save() {
+ save(notify) {
this.saving = true;
(this as any).api('i/update', {
@@ -134,7 +160,8 @@ export default Vue.extend({
birthday: this.birthday || null,
avatarId: this.avatarId,
bannerId: this.bannerId,
- isCat: this.isCat
+ isCat: this.isCat,
+ isLocked: this.isLocked
}).then(i => {
this.saving = false;
this.$store.state.i.avatarId = i.avatarId;
@@ -142,7 +169,9 @@ export default Vue.extend({
this.$store.state.i.bannerId = i.bannerId;
this.$store.state.i.bannerUrl = i.bannerUrl;
- alert('%i18n:@saved%');
+ if (notify) {
+ alert('%i18n:@saved%');
+ }
});
}
}
diff --git a/src/client/app/mobile/views/pages/user-lists.vue b/src/client/app/mobile/views/pages/user-lists.vue
index abd04c1496..5ee0636dea 100644
--- a/src/client/app/mobile/views/pages/user-lists.vue
+++ b/src/client/app/mobile/views/pages/user-lists.vue
@@ -43,7 +43,7 @@ export default Vue.extend({
title
});
- this.$router.push('/i/lists/' + list.id);
+ this.$router.push(`/i/lists/${list.id}`);
});
}
}
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 8918847a8f..c1082f31a9 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -16,7 +16,7 @@
</div>
<div class="title">
<h1>{{ user | userName }}</h1>
- <span class="username"><mk-acct :user="user"/></span>
+ <span class="username"><mk-acct :user="user" :detail="true" /></span>
<span class="followed" v-if="user.isFollowed">%i18n:@follows-you%</span>
</div>
<div class="description">
@@ -107,7 +107,7 @@ export default Vue.extend({
this.fetching = false;
Progress.done();
- document.title = Vue.filter('userName')(this.user) + ' | ' + (this as any).os.instanceName;
+ document.title = `${Vue.filter('userName')(this.user)} | ${(this as any).os.instanceName}`;
});
}
}
diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue
index 73ff1d5173..e9025ec816 100644
--- a/src/client/app/mobile/views/pages/user/home.photos.vue
+++ b/src/client/app/mobile/views/pages/user/home.photos.vue
@@ -26,7 +26,7 @@ export default Vue.extend({
mounted() {
(this as any).api('users/notes', {
userId: this.user.id,
- withMedia: true,
+ withFiles: true,
limit: 6
}).then(notes => {
notes.forEach(note => {
diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
index 49227790ff..74f43f2c71 100644
--- a/src/client/app/mobile/views/pages/welcome.vue
+++ b/src/client/app/mobile/views/pages/welcome.vue
@@ -1,5 +1,5 @@
<template>
-<div class="welcome">
+<div class="wgwfgvvimdjvhjfwxropcwksnzftjqes">
<div>
<img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name">
<p class="host">{{ host }}</p>
@@ -15,12 +15,53 @@
<mk-welcome-timeline/>
</div>
<div class="hashtags">
- <router-link v-for="tag in tags" :key="tag" :to="`/tags/${ tag }`" :title="tag">#{{ tag }}</router-link>
+ <mk-tag-cloud/>
+ </div>
+ <div class="photos">
+ <div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div>
</div>
<div class="stats" v-if="stats">
<span>%fa:user% {{ stats.originalUsersCount | number }}</span>
<span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
</div>
+ <div class="announcements" v-if="announcements && announcements.length > 0">
+ <article v-for="announcement in announcements">
+ <span class="title" v-html="announcement.title"></span>
+ <div v-html="announcement.text"></div>
+ </article>
+ </div>
+ <article class="about-misskey">
+ <h1>%i18n:common.intro.title%</h1>
+ <p v-html="'%i18n:common.intro.about%'"></p>
+ <section>
+ <h2>%i18n:common.intro.features%</h2>
+ <section>
+ <h3>%i18n:common.intro.rich-contents%</h3>
+ <div class="image"><img src="/assets/about/post.png" alt=""></div>
+ <p v-html="'%i18n:common.intro.rich-contents-desc%'"></p>
+ </section>
+ <section>
+ <h3>%i18n:common.intro.reaction%</h3>
+ <div class="image"><img src="/assets/about/reaction.png" alt=""></div>
+ <p v-html="'%i18n:common.intro.reaction-desc%'"></p>
+ </section>
+ <section>
+ <h3>%i18n:common.intro.ui%</h3>
+ <div class="image"><img src="/assets/about/ui.png" alt=""></div>
+ <p v-html="'%i18n:common.intro.ui-desc%'"></p>
+ </section>
+ <section>
+ <h3>%i18n:common.intro.drive%</h3>
+ <div class="image"><img src="/assets/about/drive.png" alt=""></div>
+ <p v-html="'%i18n:common.intro.drive-desc%'"></p>
+ </section>
+ </section>
+ <p v-html="'%i18n:common.intro.outro%'"></p>
+ </article>
+ <div class="info" v-if="meta">
+ <p>Version: <b>{{ meta.version }}</b></p>
+ <p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p>
+ </div>
<footer>
<small>{{ copyright }}</small>
</footer>
@@ -30,39 +71,53 @@
<script lang="ts">
import Vue from 'vue';
-import { apiUrl, copyright, host } from '../../../config';
+import { copyright, host } from '../../../config';
+import { concat } from '../../../../../prelude/array';
export default Vue.extend({
data() {
return {
- apiUrl,
+ meta: null,
copyright,
stats: null,
host,
name: 'Misskey',
description: '',
- tags: []
+ photos: [],
+ announcements: []
};
},
created() {
(this as any).os.getMeta().then(meta => {
+ this.meta = meta;
this.name = meta.name;
this.description = meta.description;
+ this.announcements = meta.broadcasts;
});
(this as any).api('stats').then(stats => {
this.stats = stats;
});
- (this as any).api('hashtags/trend').then(stats => {
- this.tags = stats.map(x => x.tag);
+ const image = [
+ 'image/jpeg',
+ 'image/png',
+ 'image/gif'
+ ];
+
+ (this as any).api('notes/local-timeline', {
+ fileType: image,
+ limit: 6
+ }).then((notes: any[]) => {
+ const files = concat(notes.map((n: any): any[] => n.files));
+ this.photos = files.filter(f => image.includes(f.type)).slice(0, 6);
});
}
});
</script>
<style lang="stylus" scoped>
-.welcome
+root(isDark)
text-align center
//background #fff
@@ -138,12 +193,21 @@ export default Vue.extend({
-webkit-overflow-scrolling touch
> .hashtags
- padding 16px 0
- border solid 2px #ddd
- border-radius 8px
+ padding 0 8px
+ height 200px
- > *
- margin 0 16px
+ > .photos
+ display grid
+ grid-template-rows 1fr 1fr 1fr
+ grid-template-columns 1fr 1fr
+ gap 8px
+ height 300px
+ margin-top 16px
+
+ > div
+ border-radius 4px
+ background-position center center
+ background-size cover
> .stats
margin 16px 0
@@ -156,6 +220,68 @@ export default Vue.extend({
> *
margin 0 8px
+ > .announcements
+ margin 16px 0
+
+ > article
+ background isDark ? rgba(30, 129, 216, 0.2) : rgba(155, 196, 232, 0.2)
+ border-radius 6px
+ color isDark ? #fff : #3f4967
+ padding 16px
+ margin 8px 0
+ font-size 12px
+
+ > .title
+ font-weight bold
+
+ > .about-misskey
+ margin 16px 0
+ padding 32px
+ font-size 14px
+ background #fff
+ border-radius 6px
+ overflow hidden
+ color #3a3e46
+
+ > h1
+ margin 0
+
+ & + p
+ margin-top 8px
+
+ > p:last-child
+ margin-bottom 0
+
+ > section
+ > h2
+ border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
+
+ > section
+ margin-bottom 16px
+ padding-bottom 16px
+ border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
+
+ > h3
+ margin-bottom 8px
+
+ > p
+ margin-bottom 0
+
+ > .image
+ > img
+ display block
+ width 100%
+ height 120px
+ object-fit cover
+
+ > .info
+ padding 16px 0
+ border solid 2px #ddd
+ border-radius 8px
+
+ > *
+ margin 0 16px
+
> footer
text-align center
color #444
@@ -165,4 +291,10 @@ export default Vue.extend({
margin 16px 0 0 0
opacity 0.7
+.wgwfgvvimdjvhjfwxropcwksnzftjqes[data-darkmode]
+ root(true)
+
+.wgwfgvvimdjvhjfwxropcwksnzftjqes:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index 469563495f..171620ae30 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -4,17 +4,21 @@ import * as nestedProperty from 'nested-property';
import MiOS from './mios';
import { hostname } from './config';
+import { erase } from '../../prelude/array';
const defaultSettings = {
home: null,
mobileHome: [],
deck: null,
+ tagTimelines: [],
fetchOnScroll: true,
showMaps: true,
showPostFormOnTopOfTl: false,
suggestRecentHashtags: true,
showClockOnHeader: true,
circleIcons: true,
+ contrastedAcct: true,
+ showFullAcct: false,
gradientWindowHeader: false,
showReplyTarget: true,
showMyRenotes: true,
@@ -24,6 +28,8 @@ const defaultSettings = {
disableViaMobile: false,
memo: null,
iLikeSushi: false,
+ rememberNoteVisibility: false,
+ defaultNoteVisibility: 'public',
games: {
reversi: {
showBoardLabels: false,
@@ -33,6 +39,7 @@ const defaultSettings = {
};
const defaultDeviceSettings = {
+ reduceMotion: false,
apiViaStream: true,
autoPopout: false,
darkmode: false,
@@ -43,7 +50,9 @@ const defaultDeviceSettings = {
debug: false,
lightmode: false,
loadRawImages: false,
- postStyle: 'standard'
+ alwaysShowNsfw: false,
+ postStyle: 'standard',
+ mobileNotificationPosition: 'bottom'
};
export default (os: MiOS) => new Vuex.Store({
@@ -194,7 +203,7 @@ export default (os: MiOS) => new Vuex.Store({
removeDeckColumn(state, id) {
state.deck.columns = state.deck.columns.filter(c => c.id != id);
- state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
+ state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
},
@@ -265,7 +274,7 @@ export default (os: MiOS) => new Vuex.Store({
stackLeftDeckColumn(state, id) {
const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
- state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
+ state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
const left = state.deck.layout[i - 1];
if (left) state.deck.layout[i - 1].push(id);
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
@@ -273,7 +282,7 @@ export default (os: MiOS) => new Vuex.Store({
popRightDeckColumn(state, id) {
const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
- state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
+ state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
state.deck.layout.splice(i + 1, 0, [id]);
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
},
diff --git a/src/client/app/sw.js b/src/client/app/sw.js
index ac7ea20acf..d381bfb7a5 100644
--- a/src/client/app/sw.js
+++ b/src/client/app/sw.js
@@ -3,6 +3,7 @@
*/
import composeNotification from './common/scripts/compose-notification';
+import { erase } from '../../prelude/array';
// キャッシュするリソース
const cachee = [
@@ -24,8 +25,7 @@ self.addEventListener('activate', ev => {
// Clean up old caches
ev.waitUntil(
caches.keys().then(keys => Promise.all(
- keys
- .filter(key => key != _VERSION_)
+ erase(_VERSION_, keys)
.map(key => caches.delete(key))
))
);