summaryrefslogtreecommitdiff
path: root/src/client/app/common
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/app/common')
-rw-r--r--src/client/app/common/define-widget.ts11
-rw-r--r--src/client/app/common/hotkey.ts12
-rw-r--r--src/client/app/common/scripts/check-for-update.ts13
-rw-r--r--src/client/app/common/scripts/fuck-ad-block.ts2
-rw-r--r--src/client/app/common/scripts/get-face.ts2
-rw-r--r--src/client/app/common/scripts/note-mixin.ts7
-rw-r--r--src/client/app/common/scripts/should-mute-note.ts27
-rw-r--r--src/client/app/common/scripts/stream.ts10
-rw-r--r--src/client/app/common/views/components/api-settings.vue9
-rw-r--r--src/client/app/common/views/components/autocomplete.vue42
-rw-r--r--src/client/app/common/views/components/cw-button.vue25
-rw-r--r--src/client/app/common/views/components/dialog.vue (renamed from src/client/app/common/views/components/alert.vue)62
-rw-r--r--src/client/app/common/views/components/discord-setting.vue64
-rw-r--r--src/client/app/common/views/components/emoji-picker.vue4
-rw-r--r--src/client/app/common/views/components/emoji.vue17
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.game.vue51
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.index.vue39
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.room.vue15
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.vue2
-rw-r--r--src/client/app/common/views/components/github-setting.vue64
-rw-r--r--src/client/app/common/views/components/google.vue5
-rw-r--r--src/client/app/common/views/components/image-viewer.vue1
-rw-r--r--src/client/app/common/views/components/index.ts6
-rw-r--r--src/client/app/common/views/components/integration-settings.vue103
-rw-r--r--src/client/app/common/views/components/language-settings.vue54
-rw-r--r--src/client/app/common/views/components/menu.vue8
-rw-r--r--src/client/app/common/views/components/messaging-room.message.vue11
-rw-r--r--src/client/app/common/views/components/messaging-room.vue8
-rw-r--r--src/client/app/common/views/components/messaging.vue14
-rw-r--r--src/client/app/common/views/components/mfm.ts (renamed from src/client/app/common/views/components/misskey-flavored-markdown.ts)143
-rw-r--r--src/client/app/common/views/components/misskey-flavored-markdown.vue57
-rw-r--r--src/client/app/common/views/components/mute-and-block.vue6
-rw-r--r--src/client/app/common/views/components/note-header.vue4
-rw-r--r--src/client/app/common/views/components/note-menu.vue12
-rw-r--r--src/client/app/common/views/components/password-settings.vue67
-rw-r--r--src/client/app/common/views/components/poll.vue4
-rw-r--r--src/client/app/common/views/components/profile-editor.vue56
-rw-r--r--src/client/app/common/views/components/renote.vue110
-rw-r--r--src/client/app/common/views/components/signin.vue3
-rw-r--r--src/client/app/common/views/components/theme.vue14
-rw-r--r--src/client/app/common/views/components/twitter-setting.vue65
-rw-r--r--src/client/app/common/views/components/ui/button.vue89
-rw-r--r--src/client/app/common/views/components/ui/card.vue1
-rw-r--r--src/client/app/common/views/components/ui/horizon-group.vue12
-rw-r--r--src/client/app/common/views/components/ui/input.vue59
-rw-r--r--src/client/app/common/views/components/ui/select.vue49
-rw-r--r--src/client/app/common/views/components/ui/switch.vue2
-rw-r--r--src/client/app/common/views/components/ui/textarea.vue17
-rw-r--r--src/client/app/common/views/components/user-name.vue16
-rw-r--r--src/client/app/common/views/components/welcome-timeline.vue6
-rw-r--r--src/client/app/common/views/filters/index.ts7
-rw-r--r--src/client/app/common/views/filters/number.ts5
-rw-r--r--src/client/app/common/views/filters/user.ts5
-rw-r--r--src/client/app/common/views/pages/404.vue65
-rw-r--r--src/client/app/common/views/pages/follow.vue6
-rw-r--r--src/client/app/common/views/widgets/donation.vue56
-rw-r--r--src/client/app/common/views/widgets/index.ts2
-rw-r--r--src/client/app/common/views/widgets/photo-stream.vue5
-rw-r--r--src/client/app/common/views/widgets/posts-monitor.vue2
-rw-r--r--src/client/app/common/views/widgets/server.cpu-memory.vue2
-rw-r--r--src/client/app/common/views/widgets/server.cpu.vue2
61 files changed, 1081 insertions, 556 deletions
diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts
index 56314a4104..5eb9718446 100644
--- a/src/client/app/common/define-widget.ts
+++ b/src/client/app/common/define-widget.ts
@@ -1,6 +1,6 @@
import Vue from 'vue';
-export default function<T extends object>(data: {
+export default function <T extends object>(data: {
name: string;
props?: () => T;
}) {
@@ -53,11 +53,10 @@ export default function<T extends object>(data: {
mergeProps() {
if (data.props) {
const defaultProps = data.props();
- Object.keys(defaultProps).forEach(prop => {
- if (!this.props.hasOwnProperty(prop)) {
- Vue.set(this.props, prop, defaultProps[prop]);
- }
- });
+ for (const prop of Object.keys(defaultProps)) {
+ if (this.props.hasOwnProperty(prop)) continue;
+ Vue.set(this.props, prop, defaultProps[prop]);
+ }
}
},
diff --git a/src/client/app/common/hotkey.ts b/src/client/app/common/hotkey.ts
index f7366e35cb..b2afd57ae3 100644
--- a/src/client/app/common/hotkey.ts
+++ b/src/client/app/common/hotkey.ts
@@ -28,15 +28,15 @@ const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): a
shift: false
} as pattern;
- part.trim().split('+').forEach(key => {
- key = key.trim().toLowerCase();
+ const keys = part.trim().split('+').map(x => x.trim().toLowerCase());
+ for (const key of keys) {
switch (key) {
case 'ctrl': pattern.ctrl = true; break;
case 'alt': pattern.alt = true; break;
case 'shift': pattern.shift = true; break;
default: pattern.which = keyCode(key).map(k => k.toLowerCase());
}
- });
+ }
return pattern;
});
@@ -77,11 +77,7 @@ export default {
const matched = match(e, action.patterns);
if (matched) {
- if (el._hotkey_global) {
- if (match(e, targetReservedKeys)) {
- return;
- }
- }
+ if (el._hotkey_global && match(e, targetReservedKeys)) return;
e.preventDefault();
e.stopPropagation();
diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts
index 7fe9d8d50c..20da83a0c2 100644
--- a/src/client/app/common/scripts/check-for-update.ts
+++ b/src/client/app/common/scripts/check-for-update.ts
@@ -14,19 +14,20 @@ export default async function($root: any, force = false, silent = false) {
navigator.serviceWorker.controller.postMessage('clear');
}
- navigator.serviceWorker.getRegistrations().then(registrations => {
- registrations.forEach(registration => registration.unregister());
- });
+ const registrations = await navigator.serviceWorker.getRegistrations();
+ for (const registration of registrations) {
+ registration.unregister();
+ }
} catch (e) {
console.error(e);
}
- if (!silent) {
- $root.alert({
+ /*if (!silent) {
+ $root.dialog({
title: $root.$t('@.update-available-title'),
text: $root.$t('@.update-available', { newer, current })
});
- }
+ }*/
return newer;
} else {
diff --git a/src/client/app/common/scripts/fuck-ad-block.ts b/src/client/app/common/scripts/fuck-ad-block.ts
index f5cc1b71f2..ba7e5a9f87 100644
--- a/src/client/app/common/scripts/fuck-ad-block.ts
+++ b/src/client/app/common/scripts/fuck-ad-block.ts
@@ -4,7 +4,7 @@ export default ($root: any) => {
require('fuckadblock');
function adBlockDetected() {
- $root.alert({
+ $root.dialog({
title: $root.$t('@.adblock.detected'),
text: $root.$t('@.adblock.warning')
});
diff --git a/src/client/app/common/scripts/get-face.ts b/src/client/app/common/scripts/get-face.ts
index 79cf7a1be4..b523948bd3 100644
--- a/src/client/app/common/scripts/get-face.ts
+++ b/src/client/app/common/scripts/get-face.ts
@@ -2,7 +2,7 @@ const faces = [
'(=^・・^=)',
'v(\'ω\')v',
'🐡( \'-\' 🐡 )フグパンチ!!!!',
- '🖕(´・_・`)🖕',
+ '✌️(´・_・`)✌️',
'(。>﹏<。)',
'(Δ・x・Δ)'
];
diff --git a/src/client/app/common/scripts/note-mixin.ts b/src/client/app/common/scripts/note-mixin.ts
index e0df788b34..36b8ca32c1 100644
--- a/src/client/app/common/scripts/note-mixin.ts
+++ b/src/client/app/common/scripts/note-mixin.ts
@@ -78,9 +78,10 @@ export default (opts: Opts = {}) => ({
urls(): string[] {
if (this.appearNote.text) {
const ast = parse(this.appearNote.text);
+ // TODO: 再帰的にURL要素がないか調べる
return unique(ast
- .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
- .map(t => t.url));
+ .filter(t => ((t.name == 'url' || t.name == 'link') && t.props.url && !t.props.silent))
+ .map(t => t.props.url));
} else {
return null;
}
@@ -141,7 +142,7 @@ export default (opts: Opts = {}) => ({
this.$root.api('notes/favorites/create', {
noteId: this.appearNote.id
}).then(() => {
- this.$root.alert({
+ this.$root.dialog({
type: 'success',
splash: true
});
diff --git a/src/client/app/common/scripts/should-mute-note.ts b/src/client/app/common/scripts/should-mute-note.ts
index a849135763..4eab76421d 100644
--- a/src/client/app/common/scripts/should-mute-note.ts
+++ b/src/client/app/common/scripts/should-mute-note.ts
@@ -2,27 +2,8 @@ export default function(me, settings, note) {
const isMyNote = note.userId == me.id;
const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
- if (settings.showMyRenotes === false) {
- if (isMyNote && isPureRenote) {
- return true;
- }
- }
-
- if (settings.showRenotedMyNotes === false) {
- if (isPureRenote && (note.renote.userId == me.id)) {
- return true;
- }
- }
-
- if (settings.showLocalRenotes === false) {
- if (isPureRenote && (note.renote.user.host == null)) {
- return true;
- }
- }
-
- if (!isMyNote && note.text && settings.mutedWords.some(q => !q.some(word => !note.text.includes(word)))) {
- return true;
- }
-
- return false;
+ return settings.showMyRenotes === false && isMyNote && isPureRenote ||
+ settings.showRenotedMyNotes === false && isPureRenote && note.renote.userId == me.id ||
+ settings.showLocalRenotes === false && isPureRenote && note.renote.user.host == null ||
+ !isMyNote && note.text && settings.mutedWords.some(q => q.length > 0 && !q.some(word => !note.text.includes(word)));
}
diff --git a/src/client/app/common/scripts/stream.ts b/src/client/app/common/scripts/stream.ts
index 345b112b15..23f839ae85 100644
--- a/src/client/app/common/scripts/stream.ts
+++ b/src/client/app/common/scripts/stream.ts
@@ -75,12 +75,10 @@ export default class Stream extends EventEmitter {
// チャンネル再接続
if (isReconnect) {
- this.sharedConnectionPools.forEach(p => {
+ for (const p of this.sharedConnectionPools)
p.connect();
- });
- this.nonSharedConnections.forEach(c => {
+ for (const c of this.nonSharedConnections)
c.connect();
- });
}
}
@@ -113,9 +111,9 @@ export default class Stream extends EventEmitter {
connections = [this.nonSharedConnections.find(c => c.id === id)];
}
- connections.filter(c => c != null).forEach(c => {
+ for (const c of connections.filter(c => c != null)) {
c.emit(body.type, body.body);
- });
+ }
} else {
this.emit(type, body);
}
diff --git a/src/client/app/common/views/components/api-settings.vue b/src/client/app/common/views/components/api-settings.vue
index 062218b3f4..e96eb28d93 100644
--- a/src/client/app/common/views/components/api-settings.vue
+++ b/src/client/app/common/views/components/api-settings.vue
@@ -50,10 +50,13 @@ export default Vue.extend({
methods: {
regenerateToken() {
- this.$input({
+ this.$root.dialog({
title: this.$t('enter-password'),
- type: 'password'
- }).then(password => {
+ input: {
+ type: 'password'
+ }
+ }).then(({ canceled, result: password }) => {
+ if (canceled) return;
this.$root.api('i/regenerate_token', {
password: password
});
diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index 01461c7280..e33e4ae8c5 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -3,7 +3,9 @@
<ol class="users" ref="suggests" v-if="users.length > 0">
<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1">
<img class="avatar" :src="user.avatarUrl" alt=""/>
- <span class="name">{{ user | userName }}</span>
+ <span class="name">
+ <mk-user-name :user="user"/>
+ </span>
<span class="username">@{{ user | acct }}</span>
</li>
</ol>
@@ -42,8 +44,9 @@ const lib = Object.entries(emojilib.lib).filter((x: any) => {
});
const char2file = (char: string) => {
- let codes = [...char].map(x => x.codePointAt(0).toString(16));
+ let codes = Array.from(char).map(x => x.codePointAt(0).toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
+ codes = codes.filter(x => x && x.length);
return codes.join('-');
};
@@ -54,18 +57,18 @@ const emjdb: EmojiDef[] = lib.map((x: any) => ({
url: `https://twemoji.maxcdn.com/2/svg/${char2file(x[1].char)}.svg`
}));
-lib.forEach((x: any) => {
+for (const x of lib as any) {
if (x[1].keywords) {
- x[1].keywords.forEach(k => {
+ for (const k of x[1].keywords) {
emjdb.push({
emoji: x[1].char,
name: k,
aliasOf: x[0],
url: `https://twemoji.maxcdn.com/2/svg/${char2file(x[1].char)}.svg`
});
- });
+ }
}
-});
+}
emjdb.sort((a, b) => a.name.length - b.name.length);
@@ -117,7 +120,7 @@ export default Vue.extend({
const customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
const emojiDefinitions: EmojiDef[] = [];
- customEmojis.forEach(x => {
+ for (const x of customEmojis) {
emojiDefinitions.push({
name: x.name,
emoji: `:${x.name}:`,
@@ -126,7 +129,7 @@ export default Vue.extend({
});
if (x.aliases) {
- x.aliases.forEach(alias => {
+ for (const alias of x.aliases) {
emojiDefinitions.push({
name: alias,
aliasOf: x.name,
@@ -134,9 +137,9 @@ export default Vue.extend({
url: x.url,
isCustomEmoji: true
});
- });
+ }
}
- });
+ }
emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
@@ -145,9 +148,9 @@ export default Vue.extend({
this.textarea.addEventListener('keydown', this.onKeydown);
- Array.from(document.querySelectorAll('body *')).forEach(el => {
+ for (const el of Array.from(document.querySelectorAll('body *'))) {
el.addEventListener('mousedown', this.onMousedown);
- });
+ }
this.$nextTick(() => {
this.exec();
@@ -163,18 +166,18 @@ export default Vue.extend({
beforeDestroy() {
this.textarea.removeEventListener('keydown', this.onKeydown);
- Array.from(document.querySelectorAll('body *')).forEach(el => {
+ for (const el of Array.from(document.querySelectorAll('body *'))) {
el.removeEventListener('mousedown', this.onMousedown);
- });
+ }
},
methods: {
exec() {
this.select = -1;
if (this.$refs.suggests) {
- Array.from(this.items).forEach(el => {
+ for (const el of Array.from(this.items)) {
el.removeAttribute('data-selected');
- });
+ }
}
if (this.type == 'user') {
@@ -187,7 +190,8 @@ export default Vue.extend({
} else {
this.$root.api('users/search', {
query: this.q,
- limit: 30
+ limit: 10,
+ detail: false
}).then(users => {
this.users = users;
this.fetching = false;
@@ -312,9 +316,9 @@ export default Vue.extend({
},
applySelect() {
- Array.from(this.items).forEach(el => {
+ for (const el of Array.from(this.items)) {
el.removeAttribute('data-selected');
- });
+ }
this.items[this.select].setAttribute('data-selected', 'true');
(this.items[this.select] as any).focus();
diff --git a/src/client/app/common/views/components/cw-button.vue b/src/client/app/common/views/components/cw-button.vue
index bda39f2d48..034848a116 100644
--- a/src/client/app/common/views/components/cw-button.vue
+++ b/src/client/app/common/views/components/cw-button.vue
@@ -1,21 +1,36 @@
<template>
-<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh" @click="toggle">{{ value ? this.$t('hide') : this.$t('show') }}</button>
+<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh" @click="toggle">
+ <b>{{ value ? this.$t('hide') : this.$t('show') }}</b>
+ <span v-if="!value">
+ <span v-if="note.text">{{ this.$t('chars', { count: length(note.text) }) | number }}</span>
+ <span v-if="note.text && note.files && note.files.length > 0"> / </span>
+ <span v-if="note.files && note.files.length > 0">{{ this.$t('files', { count: note.files.length }) }}</span>
+ </span>
+</button>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
+import { length } from 'stringz';
export default Vue.extend({
i18n: i18n('common/views/components/cw-button.vue'),
+
props: {
value: {
type: Boolean,
required: true
+ },
+ note: {
+ type: Object,
+ required: true
}
},
methods: {
+ length,
+
toggle() {
this.$emit('input', !this.value);
}
@@ -37,4 +52,12 @@ export default Vue.extend({
&:hover
background var(--cwButtonHoverBg)
+ > span
+ margin-left 4px
+
+ &:before
+ content '('
+ &:after
+ content ')'
+
</style>
diff --git a/src/client/app/common/views/components/alert.vue b/src/client/app/common/views/components/dialog.vue
index 27d876c87a..5cc885881b 100644
--- a/src/client/app/common/views/components/alert.vue
+++ b/src/client/app/common/views/components/dialog.vue
@@ -2,12 +2,17 @@
<div class="felqjxyj" :class="{ splash }">
<div class="bg" ref="bg" @click="onBgClick"></div>
<div class="main" ref="main">
- <div class="icon" :class="type"><fa :icon="icon"/></div>
+ <div class="icon" v-if="!input && !select && !user" :class="type"><fa :icon="icon"/></div>
<header v-if="title" v-html="title"></header>
<div class="body" v-if="text" v-html="text"></div>
- <ui-horizon-group no-grow class="buttons" v-if="!splash">
- <ui-button @click="ok" primary autofocus>OK</ui-button>
- <ui-button @click="cancel" v-if="showCancelButton">Cancel</ui-button>
+ <ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input>
+ <ui-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><span slot="prefix">@</span></ui-input>
+ <ui-select v-if="select" v-model="selectedValue">
+ <option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
+ </ui-select>
+ <ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash">
+ <ui-button @click="ok" primary :autofocus="!input && !select && !user">OK</ui-button>
+ <ui-button @click="cancel" v-if="showCancelButton || input || select || user">Cancel</ui-button>
</ui-horizon-group>
</div>
</div>
@@ -17,6 +22,7 @@
import Vue from 'vue';
import * as anime from 'animejs';
import { faTimesCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons';
+import parseAcct from "../../../../../misc/acct/parse";
export default Vue.extend({
props: {
@@ -33,6 +39,15 @@ export default Vue.extend({
type: String,
required: false
},
+ input: {
+ required: false
+ },
+ select: {
+ required: false
+ },
+ user: {
+ required: false
+ },
showCancelButton: {
type: Boolean,
default: false
@@ -43,6 +58,14 @@ export default Vue.extend({
}
},
+ data() {
+ return {
+ inputValue: this.input && this.input.default ? this.input.default : null,
+ userInputValue: null,
+ selectedValue: null
+ };
+ },
+
computed: {
icon(): any {
switch (this.type) {
@@ -82,9 +105,21 @@ export default Vue.extend({
},
methods: {
- ok() {
- this.$emit('ok');
- this.close();
+ async ok() {
+ if (this.user) {
+ const user = await this.$root.api('users/show', parseAcct(this.userInputValue));
+ if (user) {
+ this.$emit('ok', user);
+ this.close();
+ }
+ } else {
+ const result =
+ this.input ? this.inputValue :
+ this.select ? this.selectedValue :
+ true;
+ this.$emit('ok', result);
+ this.close();
+ }
},
cancel() {
@@ -114,6 +149,14 @@ export default Vue.extend({
onBgClick() {
this.cancel();
+ },
+
+ onInputKeydown(e) {
+ if (e.which == 13) { // Enter
+ e.preventDefault();
+ e.stopPropagation();
+ this.ok();
+ }
}
}
});
@@ -180,8 +223,11 @@ export default Vue.extend({
display block
margin 0 auto
+ & + header
+ margin-top 16px
+
> header
- margin 16px 0 8px 0
+ margin 0 0 8px 0
font-weight bold
font-size 20px
diff --git a/src/client/app/common/views/components/discord-setting.vue b/src/client/app/common/views/components/discord-setting.vue
deleted file mode 100644
index 113df9b0ae..0000000000
--- a/src/client/app/common/views/components/discord-setting.vue
+++ /dev/null
@@ -1,64 +0,0 @@
-<template>
-<div class="mk-discord-setting">
- <p>{{ $t('description') }}</p>
- <p class="account" v-if="$store.state.i.discord" :title="`Discord ID: ${$store.state.i.discord.id}`">{{ $t('connected-to') }}: <a :href="`https://discordapp.com/users/${$store.state.i.discord.id}`" target="_blank">@{{ $store.state.i.discord.username }}#{{ $store.state.i.discord.discriminator }}</a></p>
- <p>
- <a :href="`${apiUrl}/connect/discord`" target="_blank" @click.prevent="connect">{{ $store.state.i.discord ? this.$t('reconnect') : this.$t('connect') }}</a>
- <span v-if="$store.state.i.discord"> or </span>
- <a :href="`${apiUrl}/disconnect/discord`" target="_blank" v-if="$store.state.i.discord" @click.prevent="disconnect">{{ $t('disconnect') }}</a>
- </p>
- <p class="id" v-if="$store.state.i.discord">Discord ID: {{ $store.state.i.discord.id }}</p>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { apiUrl } from '../../../config';
-
-export default Vue.extend({
- i18n: i18n('common/views/components/discord-setting.vue'),
- data() {
- return {
- form: null,
- apiUrl
- };
- },
- mounted() {
- this.$watch('$store.state.i', () => {
- if (this.$store.state.i.discord && this.form)
- this.form.close();
- }, {
- deep: true
- });
- },
- methods: {
- connect() {
- this.form = window.open(apiUrl + '/connect/discord',
- 'discord_connect_window',
- 'height=570, width=520');
- },
-
- disconnect() {
- window.open(apiUrl + '/disconnect/discord',
- 'discord_disconnect_window',
- 'height=570, width=520');
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-discord-setting
- .account
- border solid 1px #e1e8ed
- border-radius 4px
- padding 16px
-
- a
- font-weight bold
- color inherit
-
- .id
- color #8899a6
-</style>
diff --git a/src/client/app/common/views/components/emoji-picker.vue b/src/client/app/common/views/components/emoji-picker.vue
index 8181047167..f9164ad524 100644
--- a/src/client/app/common/views/components/emoji-picker.vue
+++ b/src/client/app/common/views/components/emoji-picker.vue
@@ -114,11 +114,11 @@ export default Vue.extend({
},
onScroll(e) {
- const section = this.categories.forEach(x => {
+ for (const x of this.categories) {
const top = e.target.scrollTop;
const el = this.$refs[x.ref][0];
x.isActive = el.offsetTop <= top && el.offsetTop + el.offsetHeight > top;
- });
+ }
},
chosen(emoji) {
diff --git a/src/client/app/common/views/components/emoji.vue b/src/client/app/common/views/components/emoji.vue
index a8fef35b8a..29b09947e4 100644
--- a/src/client/app/common/views/components/emoji.vue
+++ b/src/client/app/common/views/components/emoji.vue
@@ -1,5 +1,5 @@
<template>
-<img v-if="customEmoji" class="fvgwvorwhxigeolkkrcderjzcawqrscl custom" :src="url" :alt="alt" :title="alt"/>
+<img v-if="customEmoji" class="fvgwvorwhxigeolkkrcderjzcawqrscl custom" :class="{ normal: normal }" :src="url" :alt="alt" :title="alt"/>
<img v-else-if="char && !useOsDefaultEmojis" class="fvgwvorwhxigeolkkrcderjzcawqrscl" :src="url" :alt="alt" :title="alt"/>
<span v-else-if="char && useOsDefaultEmojis">{{ char }}</span>
<span v-else>:{{ name }}:</span>
@@ -20,6 +20,11 @@ export default Vue.extend({
type: String,
required: false
},
+ normal: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
customEmojis: {
required: false,
default: () => []
@@ -61,8 +66,9 @@ export default Vue.extend({
}
if (this.char) {
- let codes = [...this.char].map(x => x.codePointAt(0).toString(16));
+ let codes = Array.from(this.char).map(x => x.codePointAt(0).toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
+ codes = codes.filter(x => x && x.length);
this.url = `https://twemoji.maxcdn.com/2/svg/${codes.join('-')}.svg`;
}
@@ -83,4 +89,11 @@ export default Vue.extend({
&:hover
transform scale(1.2)
+ &.normal
+ height 1.25em
+ vertical-align -0.25em
+
+ &:hover
+ transform none
+
</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 14c0c0891c..6d13b34c32 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
@@ -1,7 +1,7 @@
<template>
<div class="xqnhankfuuilcwvhgsopeqncafzsquya">
<button class="go-index" v-if="selfNav" @click="goIndex"><fa icon="arrow-left"/></button>
- <header><b><router-link :to="blackUser | userPage">{{ blackUser | userName }}</router-link></b>({{ $t('@.reversi.black') }}) vs <b><router-link :to="whiteUser | userPage">{{ whiteUser | userName }}</router-link></b>({{ $t('@.reversi.white') }})</header>
+ <header><b><router-link :to="blackUser | userPage"><mk-user-name :user="blackUser"/></router-link></b>({{ $t('@.reversi.black') }}) vs <b><router-link :to="whiteUser | userPage"><mk-user-name :user="whiteUser"/></router-link></b>({{ $t('@.reversi.white') }})</header>
<div style="overflow: hidden; line-height: 28px;">
<p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ $t('@.reversi.turn-of', { name: $options.filters.userName(turnUser) }) }}<mk-ellipsis/></p>
@@ -10,7 +10,7 @@
<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">{{ $t('@.reversi.my-turn') }}</p>
<p class="result" v-if="game.isEnded && logPos == logs.length">
<template v-if="game.winner">
- <span>{{ $t('@.reversi.won', { name: $options.filters.userName(game.winner) }) }}</span>
+ <misskey-flavored-markdown :text="$t('@.reversi.won', { name: $options.filters.userName(game.winner) })" :shouldBreak="false" :plainText="true" :custom-emojis="game.winner.emojis"/>
<span v-if="game.surrendered != null"> ({{ $t('surrendered') }})</span>
</template>
<template v-else>{{ $t('@.reversi.drawn') }}</template>
@@ -30,8 +30,14 @@
:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }"
@click="set(i)"
:title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`">
- <img v-if="stone === true" :src="blackUser.avatarUrl" alt="black" :class="{ contrast: $store.state.settings.games.reversi.useContrastStones }">
- <img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white" :class="{ contrast: $store.state.settings.games.reversi.useContrastStones }">
+ <template v-if="!$store.state.settings.games.reversi.useWhiteBlackStones">
+ <img v-if="stone === true" :src="blackUser.avatarUrl" alt="black" :class="{ contrast: $store.state.settings.games.reversi.useContrastStones }">
+ <img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white" :class="{ contrast: $store.state.settings.games.reversi.useContrastStones }">
+ </template>
+ <template v-if="$store.state.settings.games.reversi.useWhiteBlackStones">
+ <fa v-if="stone === true" :icon="fasCircle"/>
+ <fa v-if="stone === false" :icon="farCircle"/>
+ </template>
</div>
</div>
<div class="labels-y" v-if="this.$store.state.settings.games.reversi.showBoardLabels">
@@ -50,15 +56,13 @@
</div>
<div class="player" v-if="game.isEnded">
- <div>
- <button @click="logPos = 0" :disabled="logPos == 0"><fa icon="angle-double-left"/></button>
- <button @click="logPos--" :disabled="logPos == 0"><fa icon="angle-left"/></button>
- </div>
<span>{{ logPos }} / {{ logs.length }}</span>
- <div>
- <button @click="logPos++" :disabled="logPos == logs.length"><fa icon="angle-right"/></button>
- <button @click="logPos = logs.length" :disabled="logPos == logs.length"><fa icon="angle-double-right"/></button>
- </div>
+ <ui-horizon-group>
+ <ui-button @click="logPos = 0" :disabled="logPos == 0"><fa :icon="faAngleDoubleLeft"/></ui-button>
+ <ui-button @click="logPos--" :disabled="logPos == 0"><fa :icon="faAngleLeft"/></ui-button>
+ <ui-button @click="logPos++" :disabled="logPos == logs.length"><fa :icon="faAngleRight"/></ui-button>
+ <ui-button @click="logPos = logs.length" :disabled="logPos == logs.length"><fa :icon="faAngleDoubleRight"/></ui-button>
+ </ui-horizon-group>
</div>
<div class="info">
@@ -75,6 +79,9 @@ import i18n from '../../../../../i18n';
import * as CRC32 from 'crc-32';
import Reversi, { Color } from '../../../../../../../games/reversi/core';
import { url } from '../../../../../config';
+import { faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons';
+import { faCircle as fasCircle } from '@fortawesome/free-solid-svg-icons';
+import { faCircle as farCircle } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('common/views/components/games/reversi/reversi.game.vue'),
@@ -99,7 +106,8 @@ export default Vue.extend({
o: null as Reversi,
logs: [],
logPos: 0,
- pollingClock: null
+ pollingClock: null,
+ faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight, fasCircle, farCircle
};
},
@@ -177,9 +185,9 @@ export default Vue.extend({
loopedBoard: this.game.settings.loopedBoard
});
- this.game.logs.forEach(log => {
+ for (const log of this.game.logs) {
this.o.put(log.color, log.pos);
- });
+ }
this.logs = this.game.logs;
this.logPos = this.logs.length;
@@ -279,9 +287,9 @@ export default Vue.extend({
loopedBoard: this.game.settings.loopedBoard
});
- this.game.logs.forEach(log => {
+ for (const log of this.game.logs) {
this.o.put(log.color, log.pos, true);
- });
+ }
this.logs = this.game.logs;
this.logPos = this.logs.length;
@@ -412,6 +420,11 @@ export default Vue.extend({
&.none
border-color transparent !important
+ > svg
+ display block
+ width 100%
+ height 100%
+
> img
display block
width 100%
@@ -449,7 +462,9 @@ export default Vue.extend({
padding-bottom 16px
> .player
- padding-bottom 32px
+ padding 0 16px 32px 16px
+ margin 0 auto
+ max-width 500px
> span
display inline-block
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 b82a60a360..834702fda9 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
@@ -19,7 +19,7 @@
<h2>{{ $t('invitations') }}</h2>
<div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)">
<mk-avatar class="avatar" :user="i.parent"/>
- <span class="name"><b>{{ i.parent | userName }}</b></span>
+ <span class="name"><b><mk-user-name :user="i.parent"/></b></span>
<span class="username">@{{ i.parent.username }}</span>
<mk-time :time="i.createdAt"/>
</div>
@@ -29,7 +29,7 @@
<a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/reversi/${g.id}`">
<mk-avatar class="avatar" :user="g.user1"/>
<mk-avatar class="avatar" :user="g.user2"/>
- <span><b>{{ g.user1 | userName }}</b> vs <b>{{ g.user2 | userName }}</b></span>
+ <span><b><mk-user-name :user="g.user1"/></b> vs <b><mk-user-name :user="g.user2"/></b></span>
<span class="state">{{ g.isEnded ? $t('game-state.ended') : $t('game-state.playing') }}</span>
<mk-time :time="g.createdAt" />
</a>
@@ -39,7 +39,7 @@
<a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/reversi/${g.id}`">
<mk-avatar class="avatar" :user="g.user1"/>
<mk-avatar class="avatar" :user="g.user2"/>
- <span><b>{{ g.user1 | userName }}</b> vs <b>{{ g.user2 | userName }}</b></span>
+ <span><b><mk-user-name :user="g.user1"/></b> vs <b><mk-user-name :user="g.user2"/></b></span>
<span class="state">{{ g.isEnded ? $t('game-state.ended') : $t('game-state.playing') }}</span>
<mk-time :time="g.createdAt" />
</a>
@@ -99,23 +99,22 @@ export default Vue.extend({
this.$emit('go', game);
},
- match() {
- this.$input({
- title: this.$t('enter-username')
- }).then(username => {
- this.$root.api('users/show', {
- username
- }).then(user => {
- this.$root.api('games/reversi/match', {
- userId: user.id
- }).then(res => {
- if (res == null) {
- this.$emit('matching', user);
- } else {
- this.$emit('go', res);
- }
- });
- });
+ async match() {
+ const { result: user } = await this.$root.dialog({
+ title: this.$t('enter-username'),
+ user: {
+ local: true
+ }
+ });
+ if (user == null) return;
+ this.$root.api('games/reversi/match', {
+ userId: user.id
+ }).then(res => {
+ if (res == null) {
+ this.$emit('matching', user);
+ } else {
+ this.$emit('go', res);
+ }
});
},
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 92cdc6c083..fdbdf9b9e5 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
@@ -1,6 +1,6 @@
<template>
<div class="urbixznjwwuukfsckrwzwsqzsxornqij">
- <header><b>{{ game.user1 | userName }}</b> vs <b>{{ game.user2 | userName }}</b></header>
+ <header><b><mk-user-name :user="game.user1"/></b> vs <b><mk-user-name :user="game.user2"/></b></header>
<div>
<p>{{ $t('settings-of-the-game') }}</p>
@@ -22,8 +22,8 @@
<div v-for="(x, i) in game.settings.map.join('')"
:data-none="x == ' '"
@click="onPixelClick(i, x)">
- <template v-if="x == 'b'"><template v-if="$store.state.device.darkmode"><fa :icon="['far', 'circle']"/></template><template v-else><fa icon="circle"/></template></template>
- <template v-if="x == 'w'"><template v-if="$store.state.device.darkmode"><fa :icon="['far', 'circle']"/></template><template v-else><fa icon="circle"/></template></template>
+ <fa v-if="x == 'b'" :icon="fasCircle"/>
+ <fa v-if="x == 'w'" :icon="farCircle"/>
</div>
</div>
</div>
@@ -36,8 +36,8 @@
<div>
<form-radio v-model="game.settings.bw" value="random" @change="updateSettings">{{ $t('random') }}</form-radio>
- <form-radio v-model="game.settings.bw" :value="1" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b>{{ game.user1 | userName }}</b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
- <form-radio v-model="game.settings.bw" :value="2" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b>{{ game.user2 | userName }}</b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
+ <form-radio v-model="game.settings.bw" :value="1" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user1"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
+ <form-radio v-model="game.settings.bw" :value="2" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user2"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
</div>
</div>
@@ -117,6 +117,8 @@
import Vue from 'vue';
import i18n from '../../../../../i18n';
import * as maps from '../../../../../../../games/reversi/maps';
+import { faCircle as fasCircle } from '@fortawesome/free-solid-svg-icons';
+import { faCircle as farCircle } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('common/views/components/games/reversi/reversi.room.vue'),
@@ -129,7 +131,8 @@ export default Vue.extend({
mapName: maps.eighteight.name,
maps: maps,
form: null,
- messages: []
+ messages: [],
+ fasCircle, farCircle
};
},
diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue
index 8c555a6c4f..b6803cd7f7 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.vue
@@ -4,7 +4,7 @@
<x-gameroom :game="game" :self-nav="selfNav" @go-index="goIndex"/>
</div>
<div class="matching" v-else-if="matching">
- <h1>{{ this.$t('matching.waiting-for').split('{}')[0] }}<b>{{ matching | userName }}</b>{{ this.$t('matching.waiting-for').split('{}')[1] }}<mk-ellipsis/></h1>
+ <h1>{{ this.$t('matching.waiting-for').split('{}')[0] }}<b><mk-user-name :user="matching"/></b>{{ this.$t('matching.waiting-for').split('{}')[1] }}<mk-ellipsis/></h1>
<div class="cancel">
<form-button round @click="cancel">{{ $t('matching.cancel') }}</form-button>
</div>
diff --git a/src/client/app/common/views/components/github-setting.vue b/src/client/app/common/views/components/github-setting.vue
deleted file mode 100644
index 93d7f406f8..0000000000
--- a/src/client/app/common/views/components/github-setting.vue
+++ /dev/null
@@ -1,64 +0,0 @@
-<template>
-<div class="mk-github-setting">
- <p>{{ $t('description') }}</p>
- <p class="account" v-if="$store.state.i.github" :title="`GitHub ID: ${$store.state.i.github.id}`">{{ $t('connected-to') }}: <a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
- <p>
- <a :href="`${apiUrl}/connect/github`" target="_blank" @click.prevent="connect">{{ $store.state.i.github ? this.$t('reconnect') : this.$t('connect') }}</a>
- <span v-if="$store.state.i.github"> or </span>
- <a :href="`${apiUrl}/disconnect/github`" target="_blank" v-if="$store.state.i.github" @click.prevent="disconnect">{{ $t('disconnect') }}</a>
- </p>
- <p class="id" v-if="$store.state.i.github">GitHub ID: {{ $store.state.i.github.id }}</p>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { apiUrl } from '../../../config';
-
-export default Vue.extend({
- i18n: i18n('common/views/components/github-setting.vue'),
- data() {
- return {
- form: null,
- apiUrl
- };
- },
- mounted() {
- this.$watch('$store.state.i', () => {
- if (this.$store.state.i.github && this.form)
- this.form.close();
- }, {
- deep: true
- });
- },
- methods: {
- connect() {
- this.form = window.open(apiUrl + '/connect/github',
- 'github_connect_window',
- 'height=570, width=520');
- },
-
- disconnect() {
- window.open(apiUrl + '/disconnect/github',
- 'github_disconnect_window',
- 'height=570, width=520');
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-github-setting
- .account
- border solid 1px #e1e8ed
- border-radius 4px
- padding 16px
-
- a
- font-weight bold
- color inherit
-
- .id
- color #8899a6
-</style>
diff --git a/src/client/app/common/views/components/google.vue b/src/client/app/common/views/components/google.vue
index 1d852cf25a..dab2e6824a 100644
--- a/src/client/app/common/views/components/google.vue
+++ b/src/client/app/common/views/components/google.vue
@@ -22,7 +22,10 @@ export default Vue.extend({
},
methods: {
search() {
- window.open(`https://www.google.com/?#q=${this.query}`, '_blank');
+ const engine = this.$store.state.settings.webSearchEngine ||
+ 'https://www.google.com/?#q={{query}}';
+ const url = engine.replace('{{query}}', this.query)
+ window.open(url, '_blank');
}
}
});
diff --git a/src/client/app/common/views/components/image-viewer.vue b/src/client/app/common/views/components/image-viewer.vue
index b86a110337..204355b182 100644
--- a/src/client/app/common/views/components/image-viewer.vue
+++ b/src/client/app/common/views/components/image-viewer.vue
@@ -65,5 +65,6 @@ export default Vue.extend({
max-height 100%
margin auto
cursor zoom-out
+ image-orientation from-image
</style>
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index ace9eaf44f..40d067666a 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -1,5 +1,6 @@
import Vue from 'vue';
+import userName from './user-name.vue';
import followButton from './follow-button.vue';
import error from './error.vue';
import noteSkeleton from './note-skeleton.vue';
@@ -10,13 +11,14 @@ import trends from './trends.vue';
import analogClock from './analog-clock.vue';
import menu from './menu.vue';
import noteHeader from './note-header.vue';
+import renote from './renote.vue';
import signin from './signin.vue';
import signup from './signup.vue';
import forkit from './forkit.vue';
import acct from './acct.vue';
import avatar from './avatar.vue';
import nav from './nav.vue';
-import misskeyFlavoredMarkdown from './misskey-flavored-markdown';
+import misskeyFlavoredMarkdown from './misskey-flavored-markdown.vue';
import poll from './poll.vue';
import pollEditor from './poll-editor.vue';
import reactionIcon from './reaction-icon.vue';
@@ -43,6 +45,7 @@ import uiInfo from './ui/info.vue';
import formButton from './ui/form/button.vue';
import formRadio from './ui/form/radio.vue';
+Vue.component('mk-user-name', userName);
Vue.component('mk-follow-button', followButton);
Vue.component('mk-error', error);
Vue.component('mk-note-skeleton', noteSkeleton);
@@ -53,6 +56,7 @@ Vue.component('mk-trends', trends);
Vue.component('mk-analog-clock', analogClock);
Vue.component('mk-menu', menu);
Vue.component('mk-note-header', noteHeader);
+Vue.component('mk-renote', renote);
Vue.component('mk-signin', signin);
Vue.component('mk-signup', signup);
Vue.component('mk-forkit', forkit);
diff --git a/src/client/app/common/views/components/integration-settings.vue b/src/client/app/common/views/components/integration-settings.vue
new file mode 100644
index 0000000000..4947d7305c
--- /dev/null
+++ b/src/client/app/common/views/components/integration-settings.vue
@@ -0,0 +1,103 @@
+<template>
+<ui-card>
+ <div slot="title"><fa icon="share-alt"/> {{ $t('title') }}</div>
+
+ <section>
+ <header><fa :icon="['fab', 'twitter']"/> Twitter</header>
+ <p v-if="$store.state.i.twitter">{{ $t('connected-to') }}: <a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
+ <ui-button v-if="$store.state.i.twitter" @click="disconnectTwitter">{{ $t('disconnect') }}</ui-button>
+ <ui-button v-else @click="connectTwitter">{{ $t('connect') }}</ui-button>
+ </section>
+
+ <section>
+ <header><fa :icon="['fab', 'discord']"/> Discord</header>
+ <p v-if="$store.state.i.discord">{{ $t('connected-to') }}: <a :href="`https://discordapp.com/users/${$store.state.i.discord.id}`" target="_blank">@{{ $store.state.i.discord.username }}#{{ $store.state.i.discord.discriminator }}</a></p>
+ <ui-button v-if="$store.state.i.discord" @click="disconnectDiscord">{{ $t('disconnect') }}</ui-button>
+ <ui-button v-else @click="connectDiscord">{{ $t('connect') }}</ui-button>
+ </section>
+
+ <section>
+ <header><fa :icon="['fab', 'github']"/> GitHub</header>
+ <p v-if="$store.state.i.github">{{ $t('connected-to') }}: <a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
+ <ui-button v-if="$store.state.i.github" @click="disconnectGithub">{{ $t('disconnect') }}</ui-button>
+ <ui-button v-else @click="connectGithub">{{ $t('connect') }}</ui-button>
+ </section>
+</ui-card>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../../../i18n';
+import { apiUrl } from '../../../config';
+
+export default Vue.extend({
+ i18n: i18n('common/views/components/integration-settings.vue'),
+
+ data() {
+ return {
+ apiUrl,
+ twitterForm: null,
+ discordForm: null,
+ githubForm: null,
+ };
+ },
+
+ mounted() {
+ document.cookie = `i=${this.$store.state.i.token}`;
+ this.$watch('$store.state.i', () => {
+ if (this.$store.state.i.twitter) {
+ if (this.twitterForm) this.twitterForm.close();
+ }
+ if (this.$store.state.i.discord) {
+ if (this.discordForm) this.discordForm.close();
+ }
+ if (this.$store.state.i.github) {
+ if (this.githubForm) this.githubForm.close();
+ }
+ }, {
+ deep: true
+ });
+ },
+
+ methods: {
+ connectTwitter() {
+ this.twitterForm = window.open(apiUrl + '/connect/twitter',
+ 'twitter_connect_window',
+ 'height=570, width=520');
+ },
+
+ disconnectTwitter() {
+ window.open(apiUrl + '/disconnect/twitter',
+ 'twitter_disconnect_window',
+ 'height=570, width=520');
+ },
+
+ connectDiscord() {
+ this.discordForm = window.open(apiUrl + '/connect/discord',
+ 'discord_connect_window',
+ 'height=570, width=520');
+ },
+
+ disconnectDiscord() {
+ window.open(apiUrl + '/disconnect/discord',
+ 'discord_disconnect_window',
+ 'height=570, width=520');
+ },
+
+ connectGithub() {
+ this.githubForm = window.open(apiUrl + '/connect/github',
+ 'github_connect_window',
+ 'height=570, width=520');
+ },
+
+ disconnectGithub() {
+ window.open(apiUrl + '/disconnect/github',
+ 'github_disconnect_window',
+ 'height=570, width=520');
+ },
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+</style>
diff --git a/src/client/app/common/views/components/language-settings.vue b/src/client/app/common/views/components/language-settings.vue
new file mode 100644
index 0000000000..aa3f290511
--- /dev/null
+++ b/src/client/app/common/views/components/language-settings.vue
@@ -0,0 +1,54 @@
+<template>
+<ui-card>
+ <div slot="title"><fa icon="language"/> {{ $t('title') }}</div>
+
+ <section class="fit-top">
+ <ui-select v-model="lang" :placeholder="$t('pick-language')">
+ <optgroup :label="$t('recommended')">
+ <option value="">{{ $t('auto') }}</option>
+ </optgroup>
+
+ <optgroup :label="$t('specify-language')">
+ <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
+ </optgroup>
+ </ui-select>
+ <ui-info>Current: <i>{{ currentLanguage }}</i></ui-info>
+ <ui-info warn>{{ $t('info') }}</ui-info>
+ </section>
+</ui-card>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../../../i18n';
+import { langs } from '../../../config';
+
+export default Vue.extend({
+ i18n: i18n('common/views/components/language-settings.vue'),
+
+ data() {
+ return {
+ langs,
+ currentLanguage: 'Unknown',
+ };
+ },
+
+ computed: {
+ lang: {
+ get() { return this.$store.state.device.lang; },
+ set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
+ },
+ },
+
+ created() {
+ try {
+ const locale = JSON.parse(localStorage.getItem('locale') || "{}");
+ const localeKey = localStorage.getItem('localeKey');
+ this.currentLanguage = `${locale.meta.lang} (${localeKey})`;
+ } catch { }
+ },
+
+ methods: {
+ }
+});
+</script>
diff --git a/src/client/app/common/views/components/menu.vue b/src/client/app/common/views/components/menu.vue
index e085bf4bb9..d601c74e7d 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="onchrpzrvnoruiaenfcqvccjfuupzzwv">
+<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv" :class="{ big: $root.isMobile }">
<div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ hukidasi }" ref="popover">
<template v-for="item, i in items">
@@ -125,6 +125,11 @@ export default Vue.extend({
position initial
+ &.big
+ > .popover
+ > button
+ font-size 15px
+
> .backdrop
position fixed
top 0
@@ -180,6 +185,7 @@ export default Vue.extend({
padding 8px 16px
width 100%
color var(--popupFg)
+ white-space nowrap
&:hover
color var(--primaryForeground)
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index 966bd54170..fa77fa7af1 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -10,7 +10,8 @@
<misskey-flavored-markdown class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
<div class="file" v-if="message.file">
<a :href="message.file.url" target="_blank" :title="message.file.name">
- <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
+ <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"
+ :style="{ backgroundColor: message.file.properties.avgColor && message.file.properties.avgColor.length == 3 ? `rgb(${message.file.properties.avgColor.join(',')})` : 'transparent' }"/>
<p v-else>{{ message.file.name }}</p>
</a>
</div>
@@ -51,8 +52,8 @@ export default Vue.extend({
if (this.message.text) {
const ast = parse(this.message.text);
return unique(ast
- .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
- .map(t => t.url));
+ .filter(t => ((t.name == 'url' || t.name == 'link') && t.props.url && !t.silent))
+ .map(t => t.props.url));
} else {
return null;
}
@@ -150,7 +151,6 @@ export default Vue.extend({
> a
display block
max-width 100%
- max-height 512px
border-radius 16px
overflow hidden
text-decoration none
@@ -165,7 +165,8 @@ export default Vue.extend({
display block
margin 0
width 100%
- height 100%
+ max-height 512px
+ object-fit contain
> p
padding 30px
diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue
index b6132ceeb0..29aacd3bae 100644
--- a/src/client/app/common/views/components/messaging-room.vue
+++ b/src/client/app/common/views/components/messaging-room.vue
@@ -196,12 +196,12 @@ export default Vue.extend({
onRead(ids) {
if (!Array.isArray(ids)) ids = [ids];
- ids.forEach(id => {
+ for (const id of ids) {
if (this.messages.some(x => x.id == id)) {
const exist = this.messages.map(x => x.id).indexOf(id);
this.messages[exist].isRead = true;
}
- });
+ }
},
isBottom() {
@@ -248,13 +248,13 @@ export default Vue.extend({
onVisibilitychange() {
if (document.hidden) return;
- this.messages.forEach(message => {
+ for (const message of this.messages) {
if (message.userId !== this.$store.state.i.id && !message.isRead) {
this.connection.send('read', {
id: message.id
});
}
- });
+ }
}
}
});
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index 5b3fc790d4..9683ca0ca3 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -14,7 +14,7 @@
tabindex="-1"
>
<mk-avatar class="avatar" :user="user"/>
- <span class="name">{{ user | userName }}</span>
+ <span class="name"><mk-user-name :user="user"/></span>
<span class="username">@{{ user | acct }}</span>
</li>
</ol>
@@ -33,7 +33,7 @@
<div>
<mk-avatar class="avatar" :user="isMe(message) ? message.recipient : message.user"/>
<header>
- <span class="name">{{ isMe(message) ? message.recipient : message.user | userName }}</span>
+ <span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span>
<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
<mk-time :time="message.createdAt"/>
</header>
@@ -103,10 +103,10 @@ export default Vue.extend({
this.messages.unshift(message);
},
onRead(ids) {
- ids.forEach(id => {
+ for (const id of ids) {
const found = this.messages.find(m => m.id == id);
if (found) found.isRead = true;
- });
+ }
},
search() {
if (this.q == '') {
@@ -115,9 +115,11 @@ export default Vue.extend({
}
this.$root.api('users/search', {
query: this.q,
- max: 5
+ localOnly: true,
+ limit: 10,
+ detail: false
}).then(users => {
- this.result = users;
+ this.result = users.filter(user => user.id != this.$store.state.i.id);
});
},
navigate(user) {
diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.ts b/src/client/app/common/views/components/mfm.ts
index 1eb738813e..a6487aa4fb 100644
--- a/src/client/app/common/views/components/misskey-flavored-markdown.ts
+++ b/src/client/app/common/views/components/mfm.ts
@@ -1,11 +1,24 @@
import Vue, { VNode } from 'vue';
import { length } from 'stringz';
+import { Node } from '../../../../../mfm/parser';
import parse from '../../../../../mfm/parse';
-import getAcct from '../../../../../misc/acct/render';
import MkUrl from './url.vue';
-import { concat } from '../../../../../prelude/array';
+import { concat, sum } from '../../../../../prelude/array';
import MkFormula from './formula.vue';
import MkGoogle from './google.vue';
+import { toUnicode } from 'punycode';
+import syntaxHighlight from '../../../../../mfm/syntax-highlight';
+
+function getTextCount(tokens: Node[]): number {
+ const rootCount = sum(tokens.filter(x => x.name === 'text').map(x => length(x.props.text)));
+ const childrenCount = sum(tokens.filter(x => x.children).map(x => getTextCount(x.children)));
+ return rootCount + childrenCount;
+}
+
+function getChildrenCount(tokens: Node[]): number {
+ const countTree = tokens.filter(x => x.children).map(x => getChildrenCount(x.children));
+ return countTree.length + sum(countTree);
+}
export default Vue.component('misskey-flavored-markdown', {
props: {
@@ -21,6 +34,14 @@ export default Vue.component('misskey-flavored-markdown', {
type: Boolean,
default: true
},
+ plainText: {
+ type: Boolean,
+ default: false
+ },
+ author: {
+ type: Object,
+ default: null
+ },
i: {
type: Object,
default: null
@@ -31,23 +52,19 @@ export default Vue.component('misskey-flavored-markdown', {
},
render(createElement) {
- let ast: any[];
+ if (this.text == null || this.text == '') return;
- if (this.ast == null) {
- // Parse text to ast
- ast = parse(this.text);
- } else {
- ast = this.ast as any[];
- }
+ const ast = this.ast == null ?
+ parse(this.text, this.plainText) : // Parse text to ast
+ this.ast as Node[];
let bigCount = 0;
let motionCount = 0;
- // Parse ast to DOM
- const els = concat(ast.map((token): VNode[] => {
- switch (token.type) {
+ const genEl = (ast: Node[]) => concat(ast.map((token): VNode[] => {
+ switch (token.name) {
case 'text': {
- const text = token.content.replace(/(\r\n|\n|\r)/g, '\n');
+ const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
if (this.shouldBreak) {
const x = text.split('\n')
@@ -60,12 +77,24 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'bold': {
- return [createElement('b', token.bold)];
+ return [createElement('b', genEl(token.children))];
+ }
+
+ case 'strike': {
+ return [createElement('del', genEl(token.children))];
+ }
+
+ case 'italic': {
+ return (createElement as any)('i', {
+ attrs: {
+ style: 'font-style: oblique;'
+ },
+ }, genEl(token.children));
}
case 'big': {
bigCount++;
- const isLong = length(token.big) > 10;
+ const isLong = getTextCount(token.children) > 10 || getChildrenCount(token.children) > 5;
const isMany = bigCount > 3;
return (createElement as any)('strong', {
attrs: {
@@ -75,12 +104,24 @@ export default Vue.component('misskey-flavored-markdown', {
name: 'animate-css',
value: { classes: 'tada', iteration: 'infinite' }
}]
- }, token.big);
+ }, genEl(token.children));
+ }
+
+ case 'small': {
+ return [createElement('small', genEl(token.children))];
+ }
+
+ case 'center': {
+ return [createElement('div', {
+ attrs: {
+ style: 'text-align:center;'
+ }
+ }, genEl(token.children))];
}
case 'motion': {
motionCount++;
- const isLong = length(token.motion) > 10;
+ const isLong = getTextCount(token.children) > 10 || getChildrenCount(token.children) > 5;
const isMany = motionCount > 3;
return (createElement as any)('span', {
attrs: {
@@ -90,13 +131,14 @@ export default Vue.component('misskey-flavored-markdown', {
name: 'animate-css',
value: { classes: 'rubberBand', iteration: 'infinite' }
}]
- }, token.motion);
+ }, genEl(token.children));
}
case 'url': {
return [createElement(MkUrl, {
+ key: Math.random(),
props: {
- url: token.content,
+ url: token.props.url,
target: '_blank',
style: 'color:var(--mfmLink);'
}
@@ -107,75 +149,75 @@ export default Vue.component('misskey-flavored-markdown', {
return [createElement('a', {
attrs: {
class: 'link',
- href: token.url,
+ href: token.props.url,
target: '_blank',
- title: token.url,
+ title: token.props.url,
style: 'color:var(--mfmLink);'
}
- }, token.title)];
+ }, genEl(token.children))];
}
case 'mention': {
+ const host = token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host;
+ const canonical = host != null ? `@${token.props.username}@${toUnicode(host)}` : `@${token.props.username}`;
return (createElement as any)('router-link', {
+ key: Math.random(),
attrs: {
- to: `/${token.canonical}`,
- dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token),
+ to: `/${canonical}`,
+ // TODO
+ //dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token),
style: 'color:var(--mfmMention);'
},
directives: [{
name: 'user-preview',
- value: token.canonical
+ value: canonical
}]
- }, token.canonical);
+ }, canonical);
}
case 'hashtag': {
return [createElement('router-link', {
+ key: Math.random(),
attrs: {
- to: `/tags/${encodeURIComponent(token.hashtag)}`,
+ to: `/tags/${encodeURIComponent(token.props.hashtag)}`,
style: 'color:var(--mfmHashtag);'
}
- }, token.content)];
+ }, `#${token.props.hashtag}`)];
}
- case 'code': {
+ case 'blockCode': {
return [createElement('pre', {
class: 'code'
}, [
createElement('code', {
domProps: {
- innerHTML: token.html
+ innerHTML: syntaxHighlight(token.props.code)
}
})
])];
}
- case 'inline-code': {
+ case 'inlineCode': {
return [createElement('code', {
domProps: {
- innerHTML: token.html
+ innerHTML: syntaxHighlight(token.props.code)
}
})];
}
case 'quote': {
- const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n');
-
if (this.shouldBreak) {
- const x = text2.split('\n')
- .map(t => [createElement('span', t), createElement('br')]);
- x[x.length - 1].pop();
return [createElement('div', {
attrs: {
class: 'quote'
}
- }, x)];
+ }, genEl(token.children))];
} else {
return [createElement('span', {
attrs: {
class: 'quote'
}
- }, text2.replace(/\n/g, ' '))];
+ }, genEl(token.children))];
}
}
@@ -184,18 +226,20 @@ export default Vue.component('misskey-flavored-markdown', {
attrs: {
class: 'title'
}
- }, token.title)];
+ }, genEl(token.children))];
}
case 'emoji': {
const customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
return [createElement('mk-emoji', {
+ key: Math.random(),
attrs: {
- emoji: token.emoji,
- name: token.name
+ emoji: token.props.emoji,
+ name: token.props.name
},
props: {
- customEmojis: this.customEmojis || customEmojis
+ customEmojis: this.customEmojis || customEmojis,
+ normal: this.plainText
}
})];
}
@@ -203,8 +247,9 @@ export default Vue.component('misskey-flavored-markdown', {
case 'math': {
//const MkFormula = () => import('./formula.vue').then(m => m.default);
return [createElement(MkFormula, {
+ key: Math.random(),
props: {
- formula: token.formula
+ formula: token.props.formula
}
})];
}
@@ -212,22 +257,22 @@ export default Vue.component('misskey-flavored-markdown', {
case 'search': {
//const MkGoogle = () => import('./google.vue').then(m => m.default);
return [createElement(MkGoogle, {
+ key: Math.random(),
props: {
- q: token.query
+ q: token.props.query
}
})];
}
default: {
- console.log('unknown ast type:', token.type);
+ console.log('unknown ast type:', token.name);
return [];
}
}
}));
- // 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);
+ // Parse ast to DOM
+ return createElement('span', genEl(ast));
}
});
diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.vue b/src/client/app/common/views/components/misskey-flavored-markdown.vue
new file mode 100644
index 0000000000..65d6464d18
--- /dev/null
+++ b/src/client/app/common/views/components/misskey-flavored-markdown.vue
@@ -0,0 +1,57 @@
+<template>
+<mfm v-bind="$attrs" class="havbbuyv"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Mfm from './mfm';
+
+export default Vue.extend({
+ components: {
+ Mfm
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.havbbuyv
+ >>> .title
+ display block
+ margin-bottom 4px
+ padding 4px
+ font-size 90%
+ text-align center
+ background var(--mfmTitleBg)
+ border-radius 4px
+
+ >>> .code
+ margin 8px 0
+
+ >>> .quote
+ margin 8px
+ padding 6px 0 6px 12px
+ color var(--mfmQuote)
+ border-left solid 3px var(--mfmQuoteLine)
+
+ >>> code
+ padding 4px 8px
+ margin 0 0.5em
+ font-size 80%
+ color #525252
+ background rgba(0, 0, 0, 0.05)
+ border-radius 2px
+
+ >>> pre > code
+ padding 16px
+ margin 0
+
+ >>> [data-is-me]:after
+ content "you"
+ padding 0 4px
+ margin-left 4px
+ font-size 80%
+ color var(--primaryForeground)
+ background var(--primary)
+ border-radius 4px
+
+</style>
diff --git a/src/client/app/common/views/components/mute-and-block.vue b/src/client/app/common/views/components/mute-and-block.vue
index fdeaa97eb4..97e992ace1 100644
--- a/src/client/app/common/views/components/mute-and-block.vue
+++ b/src/client/app/common/views/components/mute-and-block.vue
@@ -7,7 +7,7 @@
<ui-info v-if="!muteFetching && mute.length == 0">{{ $t('no-muted-users') }}</ui-info>
<div class="users" v-if="mute.length != 0">
<div v-for="user in mute" :key="user.id">
- <p><b>{{ user | userName }}</b> @{{ user | acct }}</p>
+ <p><b><mk-user-name :user="user"/></b> @{{ user | acct }}</p>
</div>
</div>
</section>
@@ -17,7 +17,7 @@
<ui-info v-if="!blockFetching && block.length == 0">{{ $t('no-blocked-users') }}</ui-info>
<div class="users" v-if="block.length != 0">
<div v-for="user in block" :key="user.id">
- <p><b>{{ user | userName }}</b> @{{ user | acct }}</p>
+ <p><b><mk-user-name :user="user"/></b> @{{ user | acct }}</p>
</div>
</div>
</section>
@@ -72,7 +72,7 @@ export default Vue.extend({
methods: {
save() {
- this._mutedWords = this.mutedWords.split('\n').map(line => line.split(' '));
+ this._mutedWords = this.mutedWords.split('\n').map(line => line.split(' ').filter(x => x != ''));
}
}
});
diff --git a/src/client/app/common/views/components/note-header.vue b/src/client/app/common/views/components/note-header.vue
index 1e457d2d72..664cb308e7 100644
--- a/src/client/app/common/views/components/note-header.vue
+++ b/src/client/app/common/views/components/note-header.vue
@@ -1,7 +1,9 @@
<template>
<header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu">
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
- <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
+ <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">
+ <mk-user-name :user="note.user"/>
+ </router-link>
<span class="is-admin" v-if="note.user.isAdmin">admin</span>
<span class="is-bot" v-if="note.user.isBot">bot</span>
<span class="is-cat" v-if="note.user.isCat">cat</span>
diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/app/common/views/components/note-menu.vue
index 7d15b4ed7f..b8f34beb0c 100644
--- a/src/client/app/common/views/components/note-menu.vue
+++ b/src/client/app/common/views/components/note-menu.vue
@@ -78,7 +78,7 @@ export default Vue.extend({
this.$root.api('i/pin', {
noteId: this.note.id
}).then(() => {
- this.$root.alert({
+ this.$root.dialog({
type: 'success',
splash: true
});
@@ -95,12 +95,12 @@ export default Vue.extend({
},
del() {
- this.$root.alert({
+ this.$root.dialog({
type: 'warning',
text: this.$t('delete-confirm'),
showCancelButton: true
- }).then(res => {
- if (!res) return;
+ }).then(({ canceled }) => {
+ if (canceled) return;
this.$root.api('notes/delete', {
noteId: this.note.id
@@ -114,7 +114,7 @@ export default Vue.extend({
this.$root.api('notes/favorites/create', {
noteId: this.note.id
}).then(() => {
- this.$root.alert({
+ this.$root.dialog({
type: 'success',
splash: true
});
@@ -126,7 +126,7 @@ export default Vue.extend({
this.$root.api('notes/favorites/delete', {
noteId: this.note.id
}).then(() => {
- this.$root.alert({
+ this.$root.dialog({
type: 'success',
splash: true
});
diff --git a/src/client/app/common/views/components/password-settings.vue b/src/client/app/common/views/components/password-settings.vue
index 356f8b2fa4..eb511d6213 100644
--- a/src/client/app/common/views/components/password-settings.vue
+++ b/src/client/app/common/views/components/password-settings.vue
@@ -11,33 +11,50 @@ import i18n from '../../../i18n';
export default Vue.extend({
i18n: i18n('common/views/components/password-settings.vue'),
methods: {
- reset() {
- this.$input({
+ async reset() {
+ const { canceled: canceled1, result: currentPassword } = await this.$root.dialog({
title: this.$t('enter-current-password'),
- type: 'password'
- }).then(currentPassword => {
- this.$input({
- title: this.$t('enter-new-password'),
+ input: {
type: 'password'
- }).then(newPassword => {
- this.$input({
- title: this.$t('enter-new-password-again'),
- type: 'password'
- }).then(newPassword2 => {
- if (newPassword !== newPassword2) {
- this.$root.alert({
- title: null,
- text: this.$t('not-match')
- });
- return;
- }
- this.$root.api('i/change_password', {
- currentPasword: currentPassword,
- newPassword: newPassword
- }).then(() => {
- this.$notify(this.$t('changed'));
- });
- });
+ }
+ });
+ if (canceled1) return;
+
+ const { canceled: canceled2, result: newPassword } = await this.$root.dialog({
+ title: this.$t('enter-new-password'),
+ input: {
+ type: 'password'
+ }
+ });
+ if (canceled2) return;
+
+ const { canceled: canceled3, result: newPassword2 } = await this.$root.dialog({
+ title: this.$t('enter-new-password-again'),
+ input: {
+ type: 'password'
+ }
+ });
+ if (canceled3) return;
+
+ if (newPassword !== newPassword2) {
+ this.$root.dialog({
+ title: null,
+ text: this.$t('not-match')
+ });
+ return;
+ }
+ this.$root.api('i/change_password', {
+ currentPassword,
+ newPassword
+ }).then(() => {
+ this.$root.dialog({
+ type: 'success',
+ text: this.$t('changed')
+ });
+ }).catch(() => {
+ this.$root.dialog({
+ type: 'error',
+ text: this.$t('failed')
});
});
}
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
index 8a31ec83d7..8817d88cc5 100644
--- a/src/client/app/common/views/components/poll.vue
+++ b/src/client/app/common/views/components/poll.vue
@@ -55,12 +55,12 @@ export default Vue.extend({
noteId: this.note.id,
choice: id
}).then(() => {
- this.poll.choices.forEach(c => {
+ for (const c of this.poll.choices) {
if (c.id == id) {
c.votes++;
Vue.set(c, 'isVoted', true);
}
- });
+ }
this.showResult = true;
});
}
diff --git a/src/client/app/common/views/components/profile-editor.vue b/src/client/app/common/views/components/profile-editor.vue
index 080b8d6fc3..33c53c7dc8 100644
--- a/src/client/app/common/views/components/profile-editor.vue
+++ b/src/client/app/common/views/components/profile-editor.vue
@@ -32,6 +32,12 @@
<span>{{ $t('description') }}</span>
</ui-textarea>
+ <ui-select v-model="lang">
+ <span slot="label">{{ $t('language') }}</span>
+ <span slot="icon"><fa icon="language"/></span>
+ <option v-for="lang in unique(Object.values(langmap).map(x => x.nativeName)).map(name => Object.keys(langmap).find(k => langmap[k].nativeName == name))" :value="lang" :key="lang">{{ langmap[lang].nativeName }}</option>
+ </ui-select>
+
<ui-input type="file" @change="onAvatarChange">
<span>{{ $t('avatar') }}</span>
<span slot="icon"><fa icon="image"/></span>
@@ -66,6 +72,19 @@
<ui-switch v-model="carefulBot" @change="save(false)">{{ $t('careful-bot') }}</ui-switch>
</div>
</section>
+
+ <section v-if="enableEmail">
+ <header>{{ $t('email') }}</header>
+
+ <div>
+ <template v-if="$store.state.i.email != null">
+ <ui-info v-if="$store.state.i.emailVerified">{{ $t('email-verified') }}</ui-info>
+ <ui-info v-else warn>{{ $t('email-not-verified') }}</ui-info>
+ </template>
+ <ui-input v-model="email" type="email"><span>{{ $t('email-address') }}</span></ui-input>
+ <ui-button @click="updateEmail()">{{ $t('save') }}</ui-button>
+ </div>
+ </section>
</ui-card>
</template>
@@ -74,16 +93,24 @@ import Vue from 'vue';
import i18n from '../../../i18n';
import { apiUrl, host } from '../../../config';
import { toUnicode } from 'punycode';
+import langmap from 'langmap';
+import { unique } from '../../../../../prelude/array';
export default Vue.extend({
i18n: i18n('common/views/components/profile-editor.vue'),
+
data() {
return {
+ unique,
+ langmap,
host: toUnicode(host),
+ enableEmail: false,
+ email: null,
name: null,
username: null,
location: null,
description: null,
+ lang: null,
birthday: null,
avatarId: null,
bannerId: null,
@@ -113,10 +140,15 @@ export default Vue.extend({
},
created() {
- this.name = this.$store.state.i.name || '';
+ this.$root.getMeta().then(meta => {
+ this.enableEmail = meta.enableEmail;
+ });
+ this.email = this.$store.state.i.email;
+ this.name = this.$store.state.i.name;
this.username = this.$store.state.i.username;
this.location = this.$store.state.i.profile.location;
this.description = this.$store.state.i.description;
+ this.lang = this.$store.state.i.lang;
this.birthday = this.$store.state.i.profile.birthday;
this.avatarId = this.$store.state.i.avatarId;
this.bannerId = this.$store.state.i.bannerId;
@@ -178,9 +210,10 @@ export default Vue.extend({
name: this.name || null,
location: this.location || null,
description: this.description || null,
+ lang: this.lang,
birthday: this.birthday || null,
- avatarId: this.avatarId,
- bannerId: this.bannerId,
+ avatarId: this.avatarId || undefined,
+ bannerId: this.bannerId || undefined,
isCat: !!this.isCat,
isBot: !!this.isBot,
isLocked: !!this.isLocked,
@@ -193,12 +226,27 @@ export default Vue.extend({
this.$store.state.i.bannerUrl = i.bannerUrl;
if (notify) {
- this.$root.alert({
+ this.$root.dialog({
type: 'success',
text: this.$t('saved')
});
}
});
+ },
+
+ updateEmail() {
+ this.$root.dialog({
+ title: this.$t('@.enter-password'),
+ input: {
+ type: 'password'
+ }
+ }).then(({ canceled, result: password }) => {
+ if (canceled) return;
+ this.$root.api('i/update_email', {
+ password: password,
+ email: this.email == '' ? null : this.email
+ });
+ });
}
}
});
diff --git a/src/client/app/common/views/components/renote.vue b/src/client/app/common/views/components/renote.vue
new file mode 100644
index 0000000000..eae7bd122d
--- /dev/null
+++ b/src/client/app/common/views/components/renote.vue
@@ -0,0 +1,110 @@
+<template>
+<div class="puqkfets" :class="{ mini }">
+ <mk-avatar class="avatar" :user="note.user"/>
+ <fa icon="retweet"/>
+ <i18n path="@.renoted-by" tag="span">
+ <router-link class="name" :to="note.user | userPage" v-user-preview="note.userId" place="user">
+ <mk-user-name :user="note.user"/>
+ </router-link>
+ </i18n>
+ <div class="info">
+ <span class="mobile" v-if="note.viaMobile"><fa icon="mobile-alt"/></span>
+ <mk-time :time="note.createdAt"/>
+ <span class="visibility" v-if="note.visibility != 'public'">
+ <fa v-if="note.visibility == 'home'" icon="home"/>
+ <fa v-if="note.visibility == 'followers'" icon="unlock"/>
+ <fa v-if="note.visibility == 'specified'" icon="envelope"/>
+ <fa v-if="note.visibility == 'private'" icon="lock"/>
+ </span>
+ <span class="localOnly" v-if="note.localOnly == true"><fa icon="heart"/></span>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../../../i18n';
+
+export default Vue.extend({
+ i18n: i18n(),
+ props: {
+ note: {
+ type: Object,
+ required: true
+ },
+ mini: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.puqkfets
+ display flex
+ align-items center
+ padding 16px 32px 8px 32px
+ line-height 28px
+ white-space pre
+ color var(--renoteText)
+ background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%)
+
+ &.mini
+ padding 8px 16px
+
+ @media (min-width 500px)
+ padding 16px
+
+ @media (min-width 600px)
+ padding 16px 32px
+
+ > .avatar
+ @media (min-width 500px)
+ width 28px
+ height 28px
+
+ > .avatar
+ flex-shrink 0
+ display inline-block
+ width 28px
+ height 28px
+ margin 0 8px 0 0
+ border-radius 6px
+
+ > [data-icon]
+ margin-right 4px
+
+ > span
+ overflow hidden
+ flex-shrink 1
+ text-overflow ellipsis
+ white-space nowrap
+
+ > .name
+ font-weight bold
+
+ > .info
+ margin-left auto
+ font-size 0.9em
+
+ > .mobile
+ margin-right 8px
+
+ > .mk-time
+ flex-shrink 0
+
+ > .visibility
+ margin-left 8px
+
+ [data-icon]
+ margin-right 0
+
+ > .localOnly
+ margin-left 4px
+
+ [data-icon]
+ margin-right 0
+
+</style>
diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue
index c1a7522b00..dd3d979852 100644
--- a/src/client/app/common/views/components/signin.vue
+++ b/src/client/app/common/views/components/signin.vue
@@ -67,7 +67,8 @@ export default Vue.extend({
username: this.username,
password: this.password,
token: this.user && this.user.twoFactorEnabled ? this.token : undefined
- }, true).then(() => {
+ }, true).then(res => {
+ localStorage.setItem('i', res.i);
location.reload();
}).catch(() => {
alert(this.$t('login-failed'));
diff --git a/src/client/app/common/views/components/theme.vue b/src/client/app/common/views/components/theme.vue
index 8e23d4cfa7..6a90c30214 100644
--- a/src/client/app/common/views/components/theme.vue
+++ b/src/client/app/common/views/components/theme.vue
@@ -223,7 +223,7 @@ export default Vue.extend({
try {
theme = JSON5.parse(code);
} catch (e) {
- this.$root.alert({
+ this.$root.dialog({
type: 'error',
text: this.$t('invalid-theme')
});
@@ -236,7 +236,7 @@ export default Vue.extend({
}
if (theme.id == null) {
- this.$root.alert({
+ this.$root.dialog({
type: 'error',
text: this.$t('invalid-theme')
});
@@ -244,7 +244,7 @@ export default Vue.extend({
}
if (this.$store.state.device.themes.some(t => t.id == theme.id)) {
- this.$root.alert({
+ this.$root.dialog({
type: 'info',
text: this.$t('already-installed')
});
@@ -256,7 +256,7 @@ export default Vue.extend({
key: 'themes', value: themes
});
- this.$root.alert({
+ this.$root.dialog({
type: 'success',
text: this.$t('installed').replace('{}', theme.name)
});
@@ -269,7 +269,7 @@ export default Vue.extend({
key: 'themes', value: themes
});
- this.$root.alert({
+ this.$root.dialog({
type: 'info',
text: this.$t('uninstalled').replace('{}', theme.name)
});
@@ -306,7 +306,7 @@ export default Vue.extend({
const theme = this.myTheme;
if (theme.name == null || theme.name.trim() == '') {
- this.$root.alert({
+ this.$root.dialog({
type: 'warning',
text: this.$t('theme-name-required')
});
@@ -320,7 +320,7 @@ export default Vue.extend({
key: 'themes', value: themes
});
- this.$root.alert({
+ this.$root.dialog({
type: 'success',
text: this.$t('saved')
});
diff --git a/src/client/app/common/views/components/twitter-setting.vue b/src/client/app/common/views/components/twitter-setting.vue
deleted file mode 100644
index f75bbb7fbf..0000000000
--- a/src/client/app/common/views/components/twitter-setting.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-<div class="mk-twitter-setting">
- <p>{{ $t('description') }}</p>
- <p class="account" v-if="$store.state.i.twitter" :title="`Twitter ID: ${$store.state.i.twitter.userId}`">{{ $t('connected-to') }}: <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" @click.prevent="connect">{{ $store.state.i.twitter ? this.$t('reconnect') : this.$t('connect') }}</a>
- <span v-if="$store.state.i.twitter"> or </span>
- <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter" @click.prevent="disconnect">{{ $t('disconnect') }}</a>
- </p>
- <p class="id" v-if="$store.state.i.twitter">Twitter ID: {{ $store.state.i.twitter.userId }}</p>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-import i18n from '../../../i18n';
-import { apiUrl } from '../../../config';
-
-export default Vue.extend({
- i18n: i18n('common/views/components/twitter-setting.vue'),
- data() {
- return {
- form: null,
- apiUrl
- };
- },
- mounted() {
- this.$watch('$store.state.i', () => {
- if (this.$store.state.i.twitter) {
- if (this.form) this.form.close();
- }
- }, {
- deep: true
- });
- },
- methods: {
- connect() {
- this.form = window.open(apiUrl + '/connect/twitter',
- 'twitter_connect_window',
- 'height=570, width=520');
- },
-
- disconnect() {
- window.open(apiUrl + '/disconnect/twitter',
- 'twitter_disconnect_window',
- 'height=570, width=520');
- }
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-twitter-setting
- .account
- border solid 1px #e1e8ed
- border-radius 4px
- padding 16px
-
- a
- font-weight bold
- color inherit
-
- .id
- color #8899a6
-</style>
diff --git a/src/client/app/common/views/components/ui/button.vue b/src/client/app/common/views/components/ui/button.vue
index d7d65ad87e..42bdc31713 100644
--- a/src/client/app/common/views/components/ui/button.vue
+++ b/src/client/app/common/views/components/ui/button.vue
@@ -4,8 +4,12 @@
:class="[styl, { inline, primary }]"
:type="type"
@click="$emit('click')"
+ @mousedown="onMousedown"
>
- <slot></slot>
+ <div ref="ripples" class="ripples"></div>
+ <div class="content">
+ <slot></slot>
+ </div>
</component>
</template>
@@ -56,6 +60,47 @@ export default Vue.extend({
this.$el.focus();
});
}
+ },
+ methods: {
+ onMousedown(e: MouseEvent) {
+ function distance(p, q) {
+ const sqrt = Math.sqrt, pow = Math.pow;
+ return sqrt(pow(p.x - q.x, 2) + pow(p.y - q.y, 2));
+ }
+
+ function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY) {
+ const origin = {x: circleCenterX, y: circleCenterY};
+ const dist1 = distance({x: 0, y: 0}, origin);
+ const dist2 = distance({x: boxW, y: 0}, origin);
+ const dist3 = distance({x: 0, y: boxH}, origin);
+ const dist4 = distance({x: boxW, y: boxH }, origin);
+ return Math.max(dist1, dist2, dist3, dist4) * 2;
+ }
+
+ const rect = e.target.getBoundingClientRect();
+
+ const ripple = document.createElement('div');
+ ripple.style.top = (e.clientY - rect.top - 1).toString() + 'px';
+ ripple.style.left = (e.clientX - rect.left - 1).toString() + 'px';
+
+ this.$refs.ripples.appendChild(ripple);
+
+ const circleCenterX = e.clientX - rect.left;
+ const circleCenterY = e.clientY - rect.top;
+
+ const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY);
+
+ setTimeout(() => {
+ ripple.style.transform = 'scale(' + (scale / 2) + ')';
+ }, 1);
+ setTimeout(() => {
+ ripple.style.transition = 'all 1s ease';
+ ripple.style.opacity = '0';
+ }, 1000);
+ setTimeout(() => {
+ if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple);
+ }, 2000);
+ }
}
});
</script>
@@ -79,6 +124,10 @@ export default Vue.extend({
*
pointer-events none
+ user-select none
+
+ &:disabled
+ opacity 0.7
&:focus
&:after
@@ -107,30 +156,56 @@ export default Vue.extend({
color var(--text)
background var(--buttonBg)
- &:hover
+ &:not(:disabled):hover
background var(--buttonHoverBg)
- &:active
+ &:not(:disabled):active
background var(--buttonActiveBg)
&.primary
color var(--primaryForeground)
background var(--primary)
- &:hover
+ &:not(:disabled):hover
background var(--primaryLighten5)
- &:active
+ &:not(:disabled):active
background var(--primaryDarken5)
&:not(.fill)
color var(--primary)
background none
- &:hover
+ &:not(:disabled):hover
color var(--primaryDarken5)
- &:active
+ &:not(:disabled):active
background var(--primaryAlpha03)
+ > .ripples
+ position absolute
+ z-index 0
+ top 0
+ left 0
+ width 100%
+ height 100%
+ border-radius 6px
+ overflow hidden
+
+ >>> div
+ position absolute
+ width 2px
+ height 2px
+ border-radius 100%
+ background rgba(0, 0, 0, 0.1)
+ opacity 1
+ transform scale(1)
+ transition all 0.5s cubic-bezier(0, .5, .5, 1)
+
+ &.primary > .ripples >>> div
+ background rgba(0, 0, 0, 0.15)
+
+ > .content
+ z-index 1
+
</style>
diff --git a/src/client/app/common/views/components/ui/card.vue b/src/client/app/common/views/components/ui/card.vue
index dbbf7b14a0..21ccf95aaf 100644
--- a/src/client/app/common/views/components/ui/card.vue
+++ b/src/client/app/common/views/components/ui/card.vue
@@ -22,6 +22,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
.ui-card
margin 16px
+ max-width 850px
color var(--faceText)
background var(--face)
border-radius var(--round)
diff --git a/src/client/app/common/views/components/ui/horizon-group.vue b/src/client/app/common/views/components/ui/horizon-group.vue
index 0d4eafae52..339ab790a0 100644
--- a/src/client/app/common/views/components/ui/horizon-group.vue
+++ b/src/client/app/common/views/components/ui/horizon-group.vue
@@ -27,15 +27,25 @@ export default Vue.extend({
<style lang="stylus" scoped>
.vnxwkwuf
+ margin 16px 0
+
&.inputs
margin 32px 0
+ &.fit-top
+ margin-top 0
+
+ &.fit-bottom
+ margin-bottom 0
+
&:not(.noGrow)
display flex
> *
flex 1
+ min-width 0 !important
> *:not(:last-child)
- margin-right 16px
+ margin-right 16px !important
+
</style>
diff --git a/src/client/app/common/views/components/ui/input.vue b/src/client/app/common/views/components/ui/input.vue
index 76bb34da61..d735cc1c2f 100644
--- a/src/client/app/common/views/components/ui/input.vue
+++ b/src/client/app/common/views/components/ui/input.vue
@@ -9,27 +9,32 @@
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<template v-if="type != 'file'">
<input ref="input"
- :type="type"
- v-model="v"
- :disabled="disabled"
- :required="required"
- :readonly="readonly"
- :pattern="pattern"
- :autocomplete="autocomplete"
- :spellcheck="spellcheck"
- @focus="focused = true"
- @blur="focused = false">
+ :type="type"
+ v-model="v"
+ :disabled="disabled"
+ :required="required"
+ :readonly="readonly"
+ :placeholder="placeholder"
+ :pattern="pattern"
+ :autocomplete="autocomplete"
+ :spellcheck="spellcheck"
+ @focus="focused = true"
+ @blur="focused = false"
+ @keydown="$emit('keydown', $event)"
+ >
</template>
<template v-else>
<input ref="input"
- type="text"
- :value="placeholder"
- readonly
- @click="chooseFile">
+ type="text"
+ :value="filePlaceholder"
+ readonly
+ @click="chooseFile"
+ >
<input ref="file"
- type="file"
- :value="value"
- @change="onChangeFile">
+ type="file"
+ :value="value"
+ @change="onChangeFile"
+ >
</template>
<div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
</div>
@@ -71,6 +76,15 @@ export default Vue.extend({
type: String,
required: false
},
+ placeholder: {
+ type: String,
+ required: false
+ },
+ autofocus: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
autocomplete: {
required: false
},
@@ -106,7 +120,7 @@ export default Vue.extend({
filled(): boolean {
return this.v != '' && this.v != null;
},
- placeholder(): string {
+ filePlaceholder(): string {
if (this.type != 'file') return null;
if (this.v == null) return null;
@@ -139,6 +153,12 @@ export default Vue.extend({
}
},
mounted() {
+ if (this.autofocus) {
+ this.$nextTick(() => {
+ this.$refs.input.focus();
+ });
+ }
+
this.$nextTick(() => {
if (this.$refs.prefix) {
this.$refs.label.style.left = (this.$refs.prefix.offsetLeft + this.$refs.prefix.offsetWidth) + 'px';
@@ -325,6 +345,9 @@ root(fill)
margin 6px 0
font-size 13px
+ &:empty
+ display none
+
*
margin 0
diff --git a/src/client/app/common/views/components/ui/select.vue b/src/client/app/common/views/components/ui/select.vue
index da6f9696b5..e8b45a4a29 100644
--- a/src/client/app/common/views/components/ui/select.vue
+++ b/src/client/app/common/views/components/ui/select.vue
@@ -1,15 +1,17 @@
<template>
-<div class="ui-select" :class="[{ focused, filled }, styl]">
+<div class="ui-select" :class="[{ focused, disabled, filled, inline }, styl]">
<div class="icon" ref="icon"><slot name="icon"></slot></div>
<div class="input" @click="focus">
<span class="label" ref="label"><slot name="label"></slot></span>
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<select ref="input"
- :value="v"
- :required="required"
- @input="$emit('input', $event.target.value)"
- @focus="focused = true"
- @blur="focused = false">
+ :value="v"
+ :required="required"
+ :disabled="disabled"
+ @input="$emit('input', $event.target.value)"
+ @focus="focused = true"
+ @blur="focused = false"
+ >
<slot></slot>
</select>
<div class="suffix"><slot name="suffix"></slot></div>
@@ -22,6 +24,11 @@
import Vue from 'vue';
export default Vue.extend({
+ inject: {
+ horizonGrouped: {
+ default: false
+ }
+ },
props: {
value: {
required: false
@@ -30,11 +37,22 @@ export default Vue.extend({
type: Boolean,
required: false
},
+ disabled: {
+ type: Boolean,
+ required: false
+ },
styl: {
type: String,
required: false,
default: 'line'
- }
+ },
+ inline: {
+ type: Boolean,
+ required: false,
+ default(): boolean {
+ return this.horizonGrouped;
+ }
+ },
},
data() {
return {
@@ -76,7 +94,7 @@ root(fill)
width 24px
text-align center
line-height 32px
- color rgba(#000, 0.54)
+ color var(--inputLabel)
&:not(:empty) + .input
margin-left 28px
@@ -122,7 +140,7 @@ root(fill)
transition-duration 0.3s
font-size 16px
line-height 32px
- color rgba(#000, 0.54)
+ color var(--inputLabel)
pointer-events none
//will-change transform
transform-origin top left
@@ -171,6 +189,9 @@ root(fill)
margin 6px 0
font-size 13px
+ &:empty
+ display none
+
*
margin 0
@@ -200,4 +221,14 @@ root(fill)
&:not(.fill)
root(false)
+ &.inline
+ display inline-block
+ margin 0
+
+ &.disabled
+ opacity 0.7
+
+ &, *
+ cursor not-allowed !important
+
</style>
diff --git a/src/client/app/common/views/components/ui/switch.vue b/src/client/app/common/views/components/ui/switch.vue
index c9a9cb7911..b8bd9e2fcd 100644
--- a/src/client/app/common/views/components/ui/switch.vue
+++ b/src/client/app/common/views/components/ui/switch.vue
@@ -123,7 +123,7 @@ export default Vue.extend({
> span
display block
line-height 20px
- color currentColor
+ color var(--text)
transition inherit
> p
diff --git a/src/client/app/common/views/components/ui/textarea.vue b/src/client/app/common/views/components/ui/textarea.vue
index 8ebc79e097..d265c7ac6d 100644
--- a/src/client/app/common/views/components/ui/textarea.vue
+++ b/src/client/app/common/views/components/ui/textarea.vue
@@ -1,5 +1,5 @@
<template>
-<div class="ui-textarea" :class="{ focused, filled, tall }">
+<div class="ui-textarea" :class="{ focused, filled, tall, pre }">
<div class="input">
<span class="label" ref="label"><slot></slot></span>
<textarea ref="input"
@@ -46,6 +46,11 @@ export default Vue.extend({
required: false,
default: false
},
+ pre: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
},
data() {
return {
@@ -126,6 +131,8 @@ root(fill)
> textarea
display block
width 100%
+ min-width 100%
+ max-width 100%
min-height 100px
padding 0
font inherit
@@ -143,6 +150,9 @@ root(fill)
font-size 13px
opacity 0.7
+ &:empty
+ display none
+
*
margin 0
@@ -170,6 +180,11 @@ root(fill)
> textarea
min-height 200px
+ &.pre
+ > .input
+ > textarea
+ white-space pre
+
.ui-textarea.fill
root(true)
diff --git a/src/client/app/common/views/components/user-name.vue b/src/client/app/common/views/components/user-name.vue
new file mode 100644
index 0000000000..7719357e38
--- /dev/null
+++ b/src/client/app/common/views/components/user-name.vue
@@ -0,0 +1,16 @@
+<template>
+<misskey-flavored-markdown :text="user.name || user.username" :should-break="false" :plain-text="true" :custom-emojis="user.emojis"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ user: {
+ type: Object,
+ required: true
+ }
+ }
+});
+</script>
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index cad09a11a6..84575b35d6 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -5,7 +5,9 @@
<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>
+ <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">
+ <mk-user-name :user="note.user"/>
+ </router-link>
<span class="username">@{{ note.user | acct }}</span>
<div class="info">
<router-link class="created-at" :to="note | notePage">
@@ -14,7 +16,7 @@
</div>
</header>
<div class="text">
- <misskey-flavored-markdown v-if="note.text" :text="note.text" :customEmojis="note.emojis"/>
+ <misskey-flavored-markdown v-if="note.text" :text="note.text" :author="note.user" :custom-emojis="note.emojis"/>
</div>
</div>
</div>
diff --git a/src/client/app/common/views/filters/index.ts b/src/client/app/common/views/filters/index.ts
index 1759c19c2c..3dccbfc923 100644
--- a/src/client/app/common/views/filters/index.ts
+++ b/src/client/app/common/views/filters/index.ts
@@ -1,3 +1,10 @@
+import Vue from 'vue';
+import * as JSON5 from 'json5';
+
+Vue.filter('json5', x => {
+ return JSON5.stringify(x, null, 2);
+});
+
require('./bytes');
require('./number');
require('./user');
diff --git a/src/client/app/common/views/filters/number.ts b/src/client/app/common/views/filters/number.ts
index 08f9fea805..8c799d9442 100644
--- a/src/client/app/common/views/filters/number.ts
+++ b/src/client/app/common/views/filters/number.ts
@@ -1,6 +1,3 @@
import Vue from 'vue';
-Vue.filter('number', (n) => {
- if (n == null) return 'N/A';
- return n.toLocaleString();
-});
+Vue.filter('number', n => n == null ? 'N/A' : n.toLocaleString());
diff --git a/src/client/app/common/views/filters/user.ts b/src/client/app/common/views/filters/user.ts
index e5220229b7..9d4ae5c58b 100644
--- a/src/client/app/common/views/filters/user.ts
+++ b/src/client/app/common/views/filters/user.ts
@@ -1,6 +1,7 @@
import Vue from 'vue';
import getAcct from '../../../../../misc/acct/render';
import getUserName from '../../../../../misc/get-user-name';
+import { url } from '../../../config';
Vue.filter('acct', user => {
return getAcct(user);
@@ -10,6 +11,6 @@ Vue.filter('userName', user => {
return getUserName(user);
});
-Vue.filter('userPage', (user, path?) => {
- return `/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`;
+Vue.filter('userPage', (user, path?, absolute = false) => {
+ return `${absolute ? url : ''}/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`;
});
diff --git a/src/client/app/common/views/pages/404.vue b/src/client/app/common/views/pages/404.vue
new file mode 100644
index 0000000000..5d6db50758
--- /dev/null
+++ b/src/client/app/common/views/pages/404.vue
@@ -0,0 +1,65 @@
+<template>
+<figure class="megtcxgu">
+ <img :src="src" alt="">
+ <figcaption>
+ <h1><span>Not found</span></h1>
+ <p><span>{{ $t('page-not-found') }}</span></p>
+ </figcaption>
+</figure>
+</template>
+
+<script lang="ts">
+import Vue from 'vue'
+import i18n from '../../../i18n';
+
+export default Vue.extend({
+ i18n: i18n('common/views/pages/404.vue'),
+ data() {
+ return {
+ src: ''
+ }
+ },
+ created() {
+ this.$root.getMeta().then(meta => {
+ if (meta.errorImageUrl)
+ this.src = meta.errorImageUrl;
+ });
+ }
+})
+</script>
+
+<style lang="stylus" scoped>
+.megtcxgu
+ align-items center
+ bottom 0
+ display flex
+ justify-content center
+ left 0
+ margin auto
+ position fixed
+ right 0
+ top 0
+
+ > img
+ width 500px
+
+ > figcaption
+ margin 8px
+
+ h1,
+ p
+ color var(--text)
+ display flex
+ flex-flow column
+
+ *
+ position relative
+ width 100%
+
+ @media (max-width: 767px)
+ flex-flow column
+
+ > figcaption
+ text-align center
+
+</style>
diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue
index 9db53fdf8a..854982d91a 100644
--- a/src/client/app/common/views/pages/follow.vue
+++ b/src/client/app/common/views/pages/follow.vue
@@ -6,10 +6,12 @@
<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>
+ <router-link :to="user | userPage" class="name">
+ <mk-user-name :user="user"/>
+ </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"/>
+ <misskey-flavored-markdown v-if="user.description" :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
</div>
</div>
</main>
diff --git a/src/client/app/common/views/widgets/donation.vue b/src/client/app/common/views/widgets/donation.vue
deleted file mode 100644
index 057813891c..0000000000
--- a/src/client/app/common/views/widgets/donation.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<template>
-<div>
- <mk-widget-container :show-header="false">
- <article class="dolfvtibguprpxxhfndqaosjitixjohx">
- <h1><fa icon="heart"/>{{ $t('title') }}</h1>
- <p v-if="meta">
- {{ this.$t('text').substr(0, this.$t('text').indexOf('{')) }}
- <a :href="'mailto:' + meta.maintainer.email">{{ meta.maintainer.name }}</a>
- {{ this.$t('text').substr(this.$t('text').indexOf('}') + 1) }}
- </p>
- </article>
- </mk-widget-container>
-</div>
-</template>
-
-<script lang="ts">
-import define from '../../../common/define-widget';
-import i18n from '../../../i18n';
-
-export default define({
- name: 'donation'
-}).extend({
- i18n: i18n('common/views/widgets/donation.vue'),
- data() {
- return {
- meta: null
- };
- },
- created() {
- this.$root.getMeta().then(meta => {
- this.meta = meta;
- });
- }
-});
-</script>
-
-<style lang="stylus" scoped>
-.dolfvtibguprpxxhfndqaosjitixjohx
- padding 20px
- background var(--donationBg)
- color var(--donationFg)
-
- > h1
- margin 0 0 5px 0
- font-size 1em
-
- > [data-icon]
- margin-right 0.25em
-
- > p
- display block
- z-index 1
- margin 0
- font-size 0.8em
-
-</style>
diff --git a/src/client/app/common/views/widgets/index.ts b/src/client/app/common/views/widgets/index.ts
index 7d548ef353..7fca79f1fc 100644
--- a/src/client/app/common/views/widgets/index.ts
+++ b/src/client/app/common/views/widgets/index.ts
@@ -11,7 +11,6 @@ import wCalendar from './calendar.vue';
import wPhotoStream from './photo-stream.vue';
import wSlideshow from './slideshow.vue';
import wTips from './tips.vue';
-import wDonation from './donation.vue';
import wNav from './nav.vue';
import wHashtags from './hashtags.vue';
@@ -21,7 +20,6 @@ Vue.component('mkw-calendar', wCalendar);
Vue.component('mkw-photo-stream', wPhotoStream);
Vue.component('mkw-slideshow', wSlideshow);
Vue.component('mkw-tips', wTips);
-Vue.component('mkw-donation', wDonation);
Vue.component('mkw-broadcast', wBroadcast);
Vue.component('mkw-server', wServer);
Vue.component('mkw-posts-monitor', wPostsMonitor);
diff --git a/src/client/app/common/views/widgets/photo-stream.vue b/src/client/app/common/views/widgets/photo-stream.vue
index 13bae64bd0..516c626323 100644
--- a/src/client/app/common/views/widgets/photo-stream.vue
+++ b/src/client/app/common/views/widgets/photo-stream.vue
@@ -10,7 +10,6 @@
:style="`background-image: url(${image.thumbnailUrl || image.url})`"
draggable="true"
@dragstart="onDragstart(image, $event)"
- @dragend="onDragend"
></div>
</div>
<p :class="$style.empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p>
@@ -78,10 +77,6 @@ export default define({
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('mk_drive_file', JSON.stringify(file));
},
-
- onDragend(e) {
- this.browser.isDragSource = false;
- },
}
});
</script>
diff --git a/src/client/app/common/views/widgets/posts-monitor.vue b/src/client/app/common/views/widgets/posts-monitor.vue
index 9b2cc5a6cd..1af306b881 100644
--- a/src/client/app/common/views/widgets/posts-monitor.vue
+++ b/src/client/app/common/views/widgets/posts-monitor.vue
@@ -164,7 +164,7 @@ export default define({
this.draw();
},
onStatsLog(statsLog) {
- statsLog.forEach(stats => this.onStats(stats));
+ for (const stats of statsLog) this.onStats(stats);
}
}
});
diff --git a/src/client/app/common/views/widgets/server.cpu-memory.vue b/src/client/app/common/views/widgets/server.cpu-memory.vue
index 4a0341ddcd..92e5479b1b 100644
--- a/src/client/app/common/views/widgets/server.cpu-memory.vue
+++ b/src/client/app/common/views/widgets/server.cpu-memory.vue
@@ -121,7 +121,7 @@ export default Vue.extend({
this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0);
},
onStatsLog(statsLog) {
- statsLog.reverse().forEach(stats => this.onStats(stats));
+ for (const stats of statsLog.reverse()) this.onStats(stats);
}
}
});
diff --git a/src/client/app/common/views/widgets/server.cpu.vue b/src/client/app/common/views/widgets/server.cpu.vue
index 986577c51f..c08971e11c 100644
--- a/src/client/app/common/views/widgets/server.cpu.vue
+++ b/src/client/app/common/views/widgets/server.cpu.vue
@@ -3,7 +3,7 @@
<x-pie class="pie" :value="usage"/>
<div>
<p><fa icon="microchip"/>CPU</p>
- <p>{{ meta.cpu.cores }} Cores</p>
+ <p>{{ meta.cpu.cores }} Logical cores</p>
<p>{{ meta.cpu.model }}</p>
</div>
</div>