summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2020-03-21 13:36:41 +0900
committersyuilo <syuilotan@yahoo.co.jp>2020-03-21 13:36:41 +0900
commit6fb7721798657cf842556fb63f116316365efa74 (patch)
tree6917342553156b6483b899eafc66ef8f040b8352 /src
parentUpdate CHANGELOG.md (diff)
parent12.22.0 (diff)
downloadmisskey-6fb7721798657cf842556fb63f116316365efa74.tar.gz
misskey-6fb7721798657cf842556fb63f116316365efa74.tar.bz2
misskey-6fb7721798657cf842556fb63f116316365efa74.zip
Merge branch 'develop'
Diffstat (limited to 'src')
-rw-r--r--src/@types/jsrsasign.d.ts9
-rw-r--r--src/client/app.vue82
-rw-r--r--src/client/components/date-separated-list.vue4
-rw-r--r--src/client/components/error.vue2
-rw-r--r--src/client/components/google.vue20
-rw-r--r--src/client/components/index.ts2
-rw-r--r--src/client/components/media-image.vue2
-rw-r--r--src/client/components/menu.vue6
-rw-r--r--src/client/components/note.sub.vue7
-rw-r--r--src/client/components/note.vue20
-rw-r--r--src/client/components/notes.vue22
-rw-r--r--src/client/components/notifications.vue48
-rw-r--r--src/client/components/poll-editor.vue4
-rw-r--r--src/client/components/post-form-dialog.vue3
-rw-r--r--src/client/components/post-form.vue1
-rw-r--r--src/client/components/remote-caution.vue36
-rw-r--r--src/client/components/sequential-entrance.vue40
-rwxr-xr-x[-rw-r--r--]src/client/components/signin.vue0
-rw-r--r--src/client/components/ui/button.vue1
-rw-r--r--src/client/components/ui/container.vue1
-rw-r--r--src/client/components/ui/pagination.vue4
-rw-r--r--src/client/components/ui/range.vue138
-rw-r--r--src/client/components/ui/select.vue5
-rw-r--r--src/client/components/url-preview.vue4
-rw-r--r--src/client/components/users-dialog.vue6
-rw-r--r--src/client/components/visibility-chooser.vue4
-rw-r--r--src/client/directives/size.ts2
-rw-r--r--src/client/init.ts4
-rw-r--r--src/client/mios.ts9
-rwxr-xr-x[-rw-r--r--]src/client/pages/auth.vue5
-rw-r--r--src/client/pages/drive.vue2
-rw-r--r--src/client/pages/instance/queue.queue.vue6
-rw-r--r--src/client/pages/instance/settings.vue13
-rw-r--r--src/client/pages/messaging-room.message.vue7
-rw-r--r--src/client/pages/messaging-room.vue2
-rw-r--r--src/client/pages/messaging.vue4
-rw-r--r--src/client/pages/my-settings/integration.vue7
-rw-r--r--src/client/pages/my-settings/reaction.vue38
-rw-r--r--src/client/pages/note.vue38
-rw-r--r--src/client/pages/preferences/index.vue26
-rw-r--r--src/client/pages/user/index.vue151
-rw-r--r--src/client/scripts/hotkey.ts11
-rw-r--r--src/client/store.ts2
-rw-r--r--src/client/style.scss35
-rw-r--r--src/client/themes/_dark.json59
-rw-r--r--src/client/themes/_light.json57
-rw-r--r--src/client/themes/lavender.json51
-rw-r--r--src/client/widgets/notifications.vue7
-rw-r--r--src/client/widgets/timeline.vue8
-rw-r--r--src/config/types.ts5
-rw-r--r--src/db/elasticsearch.ts4
-rw-r--r--src/mfm/toString.ts112
-rw-r--r--src/misc/app-lock.ts4
-rw-r--r--src/misc/check-hit-antenna.ts4
-rw-r--r--src/misc/count-same-renotes.ts15
-rw-r--r--src/misc/emoji-regex.ts2
-rw-r--r--src/misc/nyaize.ts35
-rw-r--r--src/models/entities/clip-note.ts2
-rw-r--r--src/models/entities/note-reaction.ts2
-rw-r--r--src/models/repositories/note.ts6
-rw-r--r--src/models/repositories/user.ts4
-rw-r--r--src/prelude/time.ts18
-rw-r--r--src/remote/activitypub/request.ts4
-rw-r--r--src/server/api/common/signin.ts8
-rw-r--r--src/server/api/endpoints/antennas/create.ts4
-rw-r--r--src/server/api/endpoints/antennas/update.ts4
-rw-r--r--src/server/api/endpoints/charts/active-users.ts7
-rw-r--r--src/server/api/endpoints/charts/drive.ts7
-rw-r--r--src/server/api/endpoints/charts/federation.ts7
-rw-r--r--src/server/api/endpoints/charts/hashtag.ts7
-rw-r--r--src/server/api/endpoints/charts/instance.ts7
-rw-r--r--src/server/api/endpoints/charts/network.ts7
-rw-r--r--src/server/api/endpoints/charts/notes.ts7
-rw-r--r--src/server/api/endpoints/charts/user/drive.ts7
-rw-r--r--src/server/api/endpoints/charts/user/following.ts7
-rw-r--r--src/server/api/endpoints/charts/user/notes.ts7
-rw-r--r--src/server/api/endpoints/charts/user/reactions.ts7
-rw-r--r--src/server/api/endpoints/charts/users.ts7
-rw-r--r--src/server/api/endpoints/stats.ts6
-rw-r--r--src/server/api/endpoints/users/search-by-username-and-host.ts7
-rw-r--r--src/server/api/endpoints/users/search.ts2
-rw-r--r--src/server/api/service/discord.ts15
-rw-r--r--src/server/api/service/github.ts14
-rw-r--r--src/server/api/service/twitter.ts14
-rw-r--r--src/server/api/stream/channels/main.ts11
-rw-r--r--src/services/add-note-to-antenna.ts2
-rw-r--r--src/services/chart/core.ts94
-rw-r--r--src/services/drive/add-file.ts3
-rw-r--r--src/services/drive/s3.ts10
-rw-r--r--src/services/note/create.ts6
-rw-r--r--src/services/note/delete.ts4
91 files changed, 799 insertions, 571 deletions
diff --git a/src/@types/jsrsasign.d.ts b/src/@types/jsrsasign.d.ts
index 55bebd9bfb..bc9d746f7e 100644
--- a/src/@types/jsrsasign.d.ts
+++ b/src/@types/jsrsasign.d.ts
@@ -171,6 +171,7 @@ declare module 'jsrsasign' {
public static getTLVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string): ASN1TLV;
+ // tslint:disable-next-line:bool-param-default
public static getVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string, removeUnusedbits?: boolean): ASN1V;
public static hextooidstr(hex: ASN1OIDV): OID;
@@ -620,9 +621,7 @@ declare module 'jsrsasign' {
public encrypt(text: string): HexString | null;
- public encryptOAEP(text: string, hash?: string, hashLen?: number): HexString | null;
-
- public encryptOAEP(text: string, hash?: (s: string) => string, hashLen?: number): HexString | null;
+ public encryptOAEP(text: string, hash?: string | ((s: string) => string), hashLen?: number): HexString | null;
//// RSA PRIVATE
@@ -638,9 +637,7 @@ declare module 'jsrsasign' {
public decrypt(ctext: HexString): string;
- public decryptOAEP(ctext: HexString, hash?: string, hashLen?: number): string | null;
-
- public encryptOAEP(ctext: HexString, hash?: (s: string) => string, hashLen?: number): string | null;
+ public decryptOAEP(ctext: HexString, hash?: string | ((s: string) => string), hashLen?: number): string | null;
//// RSA PEM
diff --git a/src/client/app.vue b/src/client/app.vue
index 48df0b9aa8..4e5dfbd18a 100644
--- a/src/client/app.vue
+++ b/src/client/app.vue
@@ -51,11 +51,7 @@
<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
</router-link>
<template v-if="$store.getters.isSignedIn">
- <button class="item _button notifications" @click="notificationsOpen = !notificationsOpen" ref="notificationButton" v-if="$store.state.device.useNotificationsPopup">
- <fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span>
- <i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i>
- </button>
- <router-link class="item notifications" active-class="active" to="/my/notifications" ref="notificationButton" v-else>
+ <router-link class="item notifications" active-class="active" to="/my/notifications" ref="notificationButton">
<fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span>
<i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i>
</router-link>
@@ -149,17 +145,12 @@
<button class="button nav _button" @click="showNav = true" ref="navButton"><fa :icon="faBars"/><i v-if="$store.getters.isSignedIn && ($store.state.i.hasUnreadSpecifiedNotes || $store.state.i.hasPendingReceivedFollowRequest || $store.state.i.hasUnreadMessagingMessage || $store.state.i.hasUnreadAnnouncement)"><fa :icon="faCircle"/></i></button>
<button v-if="$route.name === 'index'" class="button home _button" @click="top()"><fa :icon="faHome"/></button>
<button v-else class="button home _button" @click="$router.push('/')"><fa :icon="faHome"/></button>
- <button v-if="$store.getters.isSignedIn && $store.state.device.useNotificationsPopup" class="button notifications _button" @click="notificationsOpen = !notificationsOpen" ref="notificationButton2"><fa :icon="notificationsOpen ? faTimes : faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
- <button v-if="$store.getters.isSignedIn && !$store.state.device.useNotificationsPopup" class="button notifications _button" @click="$router.push('/my/notifications')" ref="notificationButton2"><fa :icon="faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
+ <button v-if="$store.getters.isSignedIn" class="button notifications _button" @click="$router.push('/my/notifications')" ref="notificationButton2"><fa :icon="faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
<button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
</div>
<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
- <transition name="zoom-in-top">
- <x-notifications v-if="notificationsOpen" class="notifications" ref="notifications"/>
- </transition>
-
<stream-indicator v-if="$store.getters.isSignedIn"/>
</div>
</template>
@@ -173,7 +164,6 @@ import { v4 as uuid } from 'uuid';
import i18n from './i18n';
import { host, instanceName } from './config';
import { search } from './scripts/search';
-import contains from './scripts/contains';
import MkToast from './components/toast.vue';
const DESKTOP_THRESHOLD = 1100;
@@ -183,7 +173,6 @@ export default Vue.extend({
components: {
XClock: () => import('./components/header-clock.vue').then(m => m.default),
- XNotifications: () => import('./components/notifications.vue').then(m => m.default),
MkButton: () => import('./components/ui/button.vue').then(m => m.default),
XDraggable: () => import('vuedraggable'),
},
@@ -194,7 +183,6 @@ export default Vue.extend({
pageKey: 0,
showNav: false,
searching: false,
- notificationsOpen: false,
accounts: [],
lists: [],
connection: null,
@@ -226,23 +214,10 @@ export default Vue.extend({
watch:{
$route(to, from) {
this.pageKey++;
- this.notificationsOpen = false;
this.showNav = false;
this.canBack = (window.history.length > 0 && !['index'].includes(to.name));
},
- notificationsOpen(open) {
- if (open) {
- for (const el of Array.from(document.querySelectorAll('*'))) {
- el.addEventListener('mousedown', this.onMousedown);
- }
- } else {
- for (const el of Array.from(document.querySelectorAll('*'))) {
- el.removeEventListener('mousedown', this.onMousedown);
- }
- }
- },
-
isDesktop() {
if (this.isDesktop) this.adjustWidgetsWidth();
}
@@ -568,15 +543,6 @@ export default Vue.extend({
this.$root.sound('notification');
},
- onMousedown(e) {
- e.preventDefault();
- if (!contains(this.$refs.notifications.$el, e.target) &&
- !contains(this.$refs.notificationButton, e.target) &&
- !contains(this.$refs.notificationButton2, e.target)
- ) this.notificationsOpen = false;
- return false;
- },
-
widgetFunc(id) {
const w = this.$refs[id][0];
if (w.func) w.func();
@@ -652,7 +618,7 @@ export default Vue.extend({
$header-height: 60px;
$nav-width: 250px;
$nav-icon-only-width: 74px;
- $main-width: 700px;
+ $main-width: 650px;
$ui-font-size: 1em;
$nav-icon-only-threshold: 1300px;
$nav-hide-threshold: 700px;
@@ -975,17 +941,21 @@ export default Vue.extend({
> main {
width: $main-width;
min-width: $main-width;
+ box-shadow: 1px 0 0 0 var(--divider), -1px 0 0 0 var(--divider);
@media (max-width: $side-hide-threshold) {
min-width: 0;
}
> .content {
- padding: 16px;
- box-sizing: border-box;
+ > * {
+ &:not(.full) {
+ padding: var(--margin) 0;
+ }
- @media (max-width: 500px) {
- padding: 8px;
+ &:not(.naked) {
+ background: var(--pageBg);
+ }
}
}
@@ -1023,6 +993,7 @@ export default Vue.extend({
> .widgets {
box-sizing: border-box;
+ margin-left: var(--margin);
@media (max-width: $side-hide-threshold) {
display: none;
@@ -1175,34 +1146,5 @@ export default Vue.extend({
}
}
}
-
- > .notifications {
- position: fixed;
- top: 32px;
- left: 0;
- right: 0;
- margin: 0 auto;
- padding: 8px 8px 0 8px;
- z-index: 10001;
- width: 350px;
- height: 400px;
- box-sizing: border-box;
- background: var(--vocsgcxy);
- -webkit-backdrop-filter: blur(12px);
- backdrop-filter: blur(12px);
- border-radius: 6px;
- box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15);
- overflow: auto;
-
- @media (max-width: 800px) {
- width: 320px;
- height: 350px;
- }
-
- @media (max-width: 500px) {
- width: 290px;
- height: 310px;
- }
- }
}
</style>
diff --git a/src/client/components/date-separated-list.vue b/src/client/components/date-separated-list.vue
index 53fd0a7c7f..d41dd9d521 100644
--- a/src/client/components/date-separated-list.vue
+++ b/src/client/components/date-separated-list.vue
@@ -1,5 +1,5 @@
<template>
-<component :is="$store.state.device.animation ? 'transition-group' : 'div'" class="sqadhkmv" name="list" tag="div" appear :data-direction="direction" :data-reversed="reversed ? 'true' : 'false'">
+<component :is="$store.state.device.animation ? 'transition-group' : 'div'" class="sqadhkmv" name="list" tag="div" :data-direction="direction" :data-reversed="reversed ? 'true' : 'false'">
<template v-for="(item, i) in items">
<slot :item="item" :i="i"></slot>
<div class="separator" :key="item.id + '_date'" v-if="showDate(i, item)">
@@ -109,8 +109,6 @@ export default Vue.extend({
line-height: 32px;
text-align: center;
font-size: 12px;
- border-radius: 64px;
- background: var(--dateLabelBg);
color: var(--dateLabelFg);
> span {
diff --git a/src/client/components/error.vue b/src/client/components/error.vue
index f4698247b2..7446a7cb5d 100644
--- a/src/client/components/error.vue
+++ b/src/client/components/error.vue
@@ -27,8 +27,6 @@ export default Vue.extend({
<style lang="scss" scoped>
.mjndxjcg {
- max-width: 350px;
- margin: 0 auto;
padding: 32px;
text-align: center;
diff --git a/src/client/components/google.vue b/src/client/components/google.vue
index 21560008f6..01dcf24bf8 100644
--- a/src/client/components/google.vue
+++ b/src/client/components/google.vue
@@ -1,12 +1,13 @@
<template>
<div class="mk-google">
<input type="search" v-model="query" :placeholder="q">
- <button @click="search"><fa icon="search"/> {{ $t('search') }}</button>
+ <button @click="search"><fa :icon="faSearch"/> {{ $t('search') }}</button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
+import { faSearch } from '@fortawesome/free-solid-svg-icons';
import i18n from '../i18n';
export default Vue.extend({
@@ -14,7 +15,8 @@ export default Vue.extend({
props: ['q'],
data() {
return {
- query: null
+ query: null,
+ faSearch
};
},
mounted() {
@@ -42,27 +44,17 @@ export default Vue.extend({
width: 100%;
height: 40px;
font-size: 16px;
- color: var(--googleSearchFg);
- background: var(--googleSearchBg);
- border: solid 1px var(--googleSearchBorder);
+ border: solid 1px var(--divider);
border-radius: 4px 0 0 4px;
-
- &:hover {
- border-color: var(--googleSearchHoverBorder);
- }
}
> button {
flex-shrink: 0;
padding: 0 16px;
- border: solid 1px var(--googleSearchBorder);
+ border: solid 1px var(--divider);
border-left: none;
border-radius: 0 4px 4px 0;
- &:hover {
- background-color: var(--googleSearchHoverButton);
- }
-
&:active {
box-shadow: 0 2px 4px rgba(#000, 0.15) inset;
}
diff --git a/src/client/components/index.ts b/src/client/components/index.ts
index 9e95fba873..87547599a9 100644
--- a/src/client/components/index.ts
+++ b/src/client/components/index.ts
@@ -9,7 +9,6 @@ import ellipsis from './ellipsis.vue';
import time from './time.vue';
import url from './url.vue';
import loading from './loading.vue';
-import SequentialEntrance from './sequential-entrance.vue';
import error from './error.vue';
import streamIndicator from './stream-indicator.vue';
@@ -23,5 +22,4 @@ Vue.component('mk-time', time);
Vue.component('mk-url', url);
Vue.component('mk-loading', loading);
Vue.component('mk-error', error);
-Vue.component('sequential-entrance', SequentialEntrance);
Vue.component('stream-indicator', streamIndicator);
diff --git a/src/client/components/media-image.vue b/src/client/components/media-image.vue
index 3bb1bda5e2..79b5150b11 100644
--- a/src/client/components/media-image.vue
+++ b/src/client/components/media-image.vue
@@ -90,7 +90,7 @@ export default Vue.extend({
> div {
background-color: var(--fg);
border-radius: 6px;
- color: var(--secondary);
+ color: var(--accentLighten);
display: inline-block;
font-size: 14px;
font-weight: bold;
diff --git a/src/client/components/menu.vue b/src/client/components/menu.vue
index 6fee809c40..74e9a29ccf 100644
--- a/src/client/components/menu.vue
+++ b/src/client/components/menu.vue
@@ -1,6 +1,6 @@
<template>
<x-popup :source="source" :no-center="noCenter" :fixed="fixed" :width="width" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap">
- <sequential-entrance class="rrevdjwt" :class="{ left: align === 'left' }" :delay="15" :direction="direction" ref="items">
+ <div class="rrevdjwt" :class="{ left: align === 'left' }" ref="items">
<template v-for="(item, i) in items.filter(item => item !== undefined)">
<div v-if="item === null" class="divider" :key="i"></div>
<span v-else-if="item.type === 'label'" class="label item" :key="i">
@@ -28,7 +28,7 @@
<i v-if="item.indicate"><fa :icon="faCircle"/></i>
</button>
</template>
- </sequential-entrance>
+ </div>
</x-popup>
</template>
@@ -91,7 +91,7 @@ export default Vue.extend({
mounted() {
if (this.viaKeyboard) {
this.$nextTick(() => {
- focusNext(this.$refs.items.$slots.default[0].elm, true);
+ focusNext(this.$refs.items.children[0], true);
});
}
},
diff --git a/src/client/components/note.sub.vue b/src/client/components/note.sub.vue
index 7f6f972896..5efbb8f1e9 100644
--- a/src/client/components/note.sub.vue
+++ b/src/client/components/note.sub.vue
@@ -1,5 +1,5 @@
<template>
-<div class="zlrxdaqttccpwhpaagdmkawtzklsccam">
+<div class="wrpstxzv" v-size="[{ max: 450 }]">
<mk-avatar class="avatar" :user="note.user"/>
<div class="main">
<x-note-header class="header" :note="note" :mini="true"/>
@@ -56,13 +56,12 @@ export default Vue.extend({
</script>
<style lang="scss" scoped>
-.zlrxdaqttccpwhpaagdmkawtzklsccam {
+.wrpstxzv {
display: flex;
padding: 16px 32px;
font-size: 0.9em;
- background: rgba(0, 0, 0, 0.03);
- @media (max-width: 450px) {
+ &.max-width_450px {
padding: 14px 16px;
}
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 909ed30235..db669309d3 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -79,7 +79,7 @@
<div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div>
</div>
</article>
- <x-sub v-for="note in replies" :key="note.id" :note="note"/>
+ <x-sub v-for="note in replies" :key="note.id" :note="note" class="reply"/>
</div>
</template>
@@ -684,6 +684,7 @@ export default Vue.extend({
.note {
position: relative;
transition: box-shadow 0.1s ease;
+ overflow: hidden;
&.max-width_500px {
font-size: 0.9em;
@@ -749,14 +750,6 @@ export default Vue.extend({
opacity: 1;
}
- > *:first-child {
- border-radius: var(--radius) var(--radius) 0 0;
- }
-
- > *:last-child {
- border-radius: 0 0 var(--radius) var(--radius);
- }
-
> .info {
display: flex;
align-items: center;
@@ -784,6 +777,11 @@ export default Vue.extend({
padding-top: 8px;
}
+ > .reply-to {
+ opacity: 0.7;
+ padding-bottom: 0;
+ }
+
> .renote {
display: flex;
align-items: center;
@@ -937,5 +935,9 @@ export default Vue.extend({
}
}
}
+
+ > .reply {
+ border-top: solid 1px var(--divider);
+ }
}
</style>
diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue
index dc93c1f6c4..bc2ae8472c 100644
--- a/src/client/components/notes.vue
+++ b/src/client/components/notes.vue
@@ -7,22 +7,22 @@
<mk-error v-if="error" @retry="init()"/>
- <div class="more" v-if="more && reversed" style="margin-bottom: var(--margin);">
- <mk-button class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()" primary>
+ <div v-if="more && reversed" style="margin-bottom: var(--margin);">
+ <button class="_panel _button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><mk-loading inline/></template>
- </mk-button>
+ </button>
</div>
<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
<x-note :note="note" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
</x-list>
- <div class="more" v-if="more && !reversed" style="margin-top: var(--margin);">
- <mk-button class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()" primary>
+ <div v-if="more && !reversed" style="margin-top: var(--margin);">
+ <button class="_panel _button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><mk-loading inline/></template>
- </mk-button>
+ </button>
</div>
</div>
</template>
@@ -111,16 +111,10 @@ export default Vue.extend({
&.max-width_500px {
> .notes {
> ::v-deep *:not(:last-child) {
- margin-bottom: var(--marginHalf);
+ //margin-bottom: var(--marginHalf);
+ margin-bottom: 0;
}
}
}
-
- > .more > .button {
- margin-left: auto;
- margin-right: auto;
- height: 48px;
- width: 100%;
- }
}
</style>
diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue
index ff6d63821f..a17663b01d 100644
--- a/src/client/components/notifications.vue
+++ b/src/client/components/notifications.vue
@@ -1,13 +1,13 @@
<template>
-<div class="mk-notifications" :class="{ page }">
+<div class="mk-notifications">
<x-list class="notifications" :items="items" v-slot="{ item: notification }">
<x-note v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" :key="notification.id"/>
- <x-notification v-else :notification="notification" :with-time="true" :full="true" class="notification" :class="{ _panel: page }" :key="notification.id"/>
+ <x-notification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
</x-list>
- <button class="more _button" v-if="more" @click="fetchMore" :disabled="moreFetching">
+ <button class="_panel _button" v-if="more" @click="fetchMore" :disabled="moreFetching">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
- <template v-if="moreFetching"><fa :icon="faSpinner" pulse fixed-width/></template>
+ <template v-if="moreFetching"><mk-loading inline/></template>
</button>
<p class="empty" v-if="empty">{{ $t('noNotifications') }}</p>
@@ -18,7 +18,6 @@
<script lang="ts">
import Vue from 'vue';
-import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import i18n from '../i18n';
import paging from '../scripts/paging';
import XNotification from './notification.vue';
@@ -43,11 +42,6 @@ export default Vue.extend({
type: String,
required: false
},
- page: {
- type: Boolean,
- required: false,
- default: false
- }
},
data() {
@@ -60,7 +54,6 @@ export default Vue.extend({
includeTypes: this.type ? [this.type] : undefined
})
},
- faSpinner
};
},
@@ -94,35 +87,10 @@ export default Vue.extend({
<style lang="scss" scoped>
.mk-notifications {
- &.page {
- > .notifications {
- > ::v-deep * {
- margin-bottom: var(--margin);
- }
- }
- }
-
- &:not(.page) {
- > .notifications {
- > ::v-deep * {
- margin-bottom: 8px;
- }
-
- > .notification {
- background: var(--panel);
- border-radius: 6px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- }
- }
- }
-
- > .more {
- display: block;
- width: 100%;
- padding: 16px;
-
- > [data-icon] {
- margin-right: 4px;
+ > .notifications {
+ > ::v-deep * {
+ //margin-bottom: var(--margin);
+ margin-bottom: 0;
}
}
diff --git a/src/client/components/poll-editor.vue b/src/client/components/poll-editor.vue
index b5b8c2c02d..91c7dab598 100644
--- a/src/client/components/poll-editor.vue
+++ b/src/client/components/poll-editor.vue
@@ -53,7 +53,7 @@ import Vue from 'vue';
import { faExclamationTriangle, faTimes } from '@fortawesome/free-solid-svg-icons';
import i18n from '../i18n';
import { erase } from '../../prelude/array';
-import { addTimespan } from '../../prelude/time';
+import { addTime } from '../../prelude/time';
import { formatDateTimeString } from '../../misc/format-time-string';
import MkInput from './ui/input.vue';
import MkSelect from './ui/select.vue';
@@ -73,7 +73,7 @@ export default Vue.extend({
choices: ['', ''],
multiple: false,
expiration: 'infinite',
- atDate: formatDateTimeString(addTimespan(new Date(), 1, 'days'), 'yyyy-MM-dd'),
+ atDate: formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'),
atTime: '00:00',
after: 0,
unit: 'second',
diff --git a/src/client/components/post-form-dialog.vue b/src/client/components/post-form-dialog.vue
index b6531474cf..9cb527af23 100644
--- a/src/client/components/post-form-dialog.vue
+++ b/src/client/components/post-form-dialog.vue
@@ -17,7 +17,8 @@
:initial-note="initialNote"
:instant="instant"
@posted="onPosted"
- @cancel="onCanceled"/>
+ @cancel="onCanceled"
+ style="border-radius: var(--radius);"/>
</transition>
</div>
</div>
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index 2d35cfe167..7b84938d5a 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -586,7 +586,6 @@ export default Vue.extend({
<style lang="scss" scoped>
.gafaadew {
background: var(--panel);
- border-radius: var(--radius);
> header {
z-index: 1000;
diff --git a/src/client/components/remote-caution.vue b/src/client/components/remote-caution.vue
new file mode 100644
index 0000000000..95b37d3053
--- /dev/null
+++ b/src/client/components/remote-caution.vue
@@ -0,0 +1,36 @@
+<template>
+<div class="jmgmzlwq _panel"><fa :icon="faExclamationTriangle" style="margin-right: 8px;"/>{{ $t('remoteUserCaution') }}<a :href="href" rel="nofollow noopener" target="_blank">{{ $t('showOnRemote') }}</a></div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../i18n';
+
+export default Vue.extend({
+ i18n,
+ props: {
+ href: {
+ type: String,
+ required: true
+ },
+ },
+ data() {
+ return {
+ faExclamationTriangle
+ };
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.jmgmzlwq {
+ font-size: 0.8em;
+ padding: 16px;
+
+ > a {
+ margin-left: 4px;
+ color: var(--accent);
+ }
+}
+</style>
diff --git a/src/client/components/sequential-entrance.vue b/src/client/components/sequential-entrance.vue
deleted file mode 100644
index 50113cff1c..0000000000
--- a/src/client/components/sequential-entrance.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-<template>
-<transition-group v-if="$store.state.device.animation"
- class="uupnnhew"
- name="staggered"
- tag="div"
- appear
->
- <slot></slot>
-</transition-group>
-<div v-else>
- <slot></slot>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
- methods: {
- focus() {
- this.$slots.default[0].elm.focus();
- }
- },
-});
-</script>
-
-<style lang="scss">
-.uupnnhew {
- > .staggered-enter {
- opacity: 0;
- transform: translateY(-64px);
- }
-
- @for $i from 1 through 30 {
- > .staggered-enter-active:nth-child(#{$i}) {
- transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1) (15ms * ($i - 1)), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1) (15ms * ($i - 1));
- }
- }
-}
-</style>
diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue
index 758bc59107..758bc59107 100644..100755
--- a/src/client/components/signin.vue
+++ b/src/client/components/signin.vue
diff --git a/src/client/components/ui/button.vue b/src/client/components/ui/button.vue
index 5264224c18..15289c820e 100644
--- a/src/client/components/ui/button.vue
+++ b/src/client/components/ui/button.vue
@@ -124,7 +124,6 @@ export default Vue.extend({
&.primary {
color: #fff;
background: var(--accent);
- box-shadow: 0 6px 16px var(--accentShadow);
&:not(:disabled):hover {
background: var(--jkhztclx);
diff --git a/src/client/components/ui/container.vue b/src/client/components/ui/container.vue
index 4e7c9420ab..9d5abdf2dd 100644
--- a/src/client/components/ui/container.vue
+++ b/src/client/components/ui/container.vue
@@ -110,6 +110,7 @@ export default Vue.extend({
> header {
position: relative;
box-shadow: 0 1px 0 0 var(--divider);
+ z-index: 1;
> .title {
margin: 0;
diff --git a/src/client/components/ui/pagination.vue b/src/client/components/ui/pagination.vue
index 7f04b35de1..e888b7420c 100644
--- a/src/client/components/ui/pagination.vue
+++ b/src/client/components/ui/pagination.vue
@@ -1,5 +1,5 @@
<template>
-<sequential-entrance class="cxiknjgy" :class="{ autoMargin }">
+<div class="cxiknjgy" :class="{ autoMargin }">
<slot :items="items"></slot>
<div class="empty" v-if="empty" key="_empty_">
<slot name="empty"></slot>
@@ -10,7 +10,7 @@
<template v-if="moreFetching"><mk-loading inline/></template>
</mk-button>
</div>
-</sequential-entrance>
+</div>
</template>
<script lang="ts">
diff --git a/src/client/components/ui/range.vue b/src/client/components/ui/range.vue
new file mode 100644
index 0000000000..7fb857f520
--- /dev/null
+++ b/src/client/components/ui/range.vue
@@ -0,0 +1,138 @@
+<template>
+<div class="timctyfi" :class="{ focused, disabled }">
+ <div class="icon"><slot name="icon"></slot></div>
+ <span class="title"><slot name="title"></slot></span>
+ <input
+ type="range"
+ ref="input"
+ v-model="v"
+ :disabled="disabled"
+ :min="min"
+ :max="max"
+ :step="step"
+ :autofocus="autofocus"
+ @focus="focused = true"
+ @blur="focused = false"
+ @input="$emit('input', $event.target.value)"
+ />
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+export default Vue.extend({
+ props: {
+ value: {
+ type: Number,
+ required: false,
+ default: 0
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ min: {
+ type: Number,
+ required: false,
+ default: 0
+ },
+ max: {
+ type: Number,
+ required: false,
+ default: 100
+ },
+ step: {
+ type: Number,
+ required: false,
+ default: 1
+ },
+ autofocus: {
+ type: Boolean,
+ required: false
+ }
+ },
+ data() {
+ return {
+ v: this.value,
+ focused: false
+ };
+ },
+ watch: {
+ value(v) {
+ this.v = parseFloat(v);
+ }
+ },
+ mounted() {
+ if (this.autofocus) {
+ this.$nextTick(() => {
+ this.$refs.input.focus();
+ });
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.timctyfi {
+ position: relative;
+ margin: 8px;
+
+ > .icon {
+ display: inline-block;
+ width: 24px;
+ text-align: center;
+ }
+
+ > .title {
+ pointer-events: none;
+ font-size: 16px;
+ color: var(--inputLabel);
+ overflow: hidden;
+ }
+
+ > input {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background: var(--xxubwiul);
+ height: 7px;
+ margin: 0 8px;
+ outline: 0;
+ border: 0;
+ border-radius: 7px;
+
+ &.disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ &::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ display: block;
+ border-radius: 50%;
+ border: none;
+ background: var(--accent);
+ box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
+ box-sizing: content-box;
+ }
+
+ &::-moz-range-thumb {
+ -moz-appearance: none;
+ appearance: none;
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ display: block;
+ border-radius: 50%;
+ border: none;
+ background: var(--accent);
+ box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
+ }
+ }
+}
+</style>
diff --git a/src/client/components/ui/select.vue b/src/client/components/ui/select.vue
index a1e89cdf02..ce21949713 100644
--- a/src/client/components/ui/select.vue
+++ b/src/client/components/ui/select.vue
@@ -158,6 +158,11 @@ export default Vue.extend({
outline: none;
box-shadow: none;
color: var(--fg);
+
+ option,
+ optgroup {
+ background: var(--bg);
+ }
}
> .prefix,
diff --git a/src/client/components/url-preview.vue b/src/client/components/url-preview.vue
index 940d3892db..94d07cbaed 100644
--- a/src/client/components/url-preview.vue
+++ b/src/client/components/url-preview.vue
@@ -230,8 +230,8 @@ export default Vue.extend({
position: relative;
display: block;
font-size: 14px;
- box-shadow: 0 1px 4px var(--tyvedwbe);
- border-radius: 4px;
+ box-shadow: 0 0 0 1px var(--divider);
+ border-radius: 6px;
overflow: hidden;
&:hover {
diff --git a/src/client/components/users-dialog.vue b/src/client/components/users-dialog.vue
index a70b3c2d13..9d0c5e4251 100644
--- a/src/client/components/users-dialog.vue
+++ b/src/client/components/users-dialog.vue
@@ -6,15 +6,15 @@
<button class="_button" @click="close()"><fa :icon="faTimes"/></button>
</div>
- <sequential-entrance class="users">
- <router-link v-for="(item, i) in items" class="user" :key="item.id" :to="extract ? extract(item) : item | userPage">
+ <div class="users">
+ <router-link v-for="item in items" class="user" :key="item.id" :to="extract ? extract(item) : item | userPage">
<mk-avatar :user="extract ? extract(item) : item" class="avatar" :disable-link="true"/>
<div class="body">
<mk-user-name :user="extract ? extract(item) : item" class="name"/>
<mk-acct :user="extract ? extract(item) : item" class="acct"/>
</div>
</router-link>
- </sequential-entrance>
+ </div>
<button class="more _button" v-if="more" @click="fetchMore" :disabled="moreFetching">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
diff --git a/src/client/components/visibility-chooser.vue b/src/client/components/visibility-chooser.vue
index 28413fd837..dc7b41e286 100644
--- a/src/client/components/visibility-chooser.vue
+++ b/src/client/components/visibility-chooser.vue
@@ -1,6 +1,6 @@
<template>
<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }">
- <sequential-entrance class="gqyayizv" :delay="30">
+ <div class="gqyayizv">
<button class="_button" @click="choose('public')" :class="{ active: v == 'public' }" data-index="1" key="public">
<div><fa :icon="faGlobe"/></div>
<div>
@@ -29,7 +29,7 @@
<span>{{ $t('_visibility.specifiedDescription') }}</span>
</div>
</button>
- </sequential-entrance>
+ </div>
</x-popup>
</template>
diff --git a/src/client/directives/size.ts b/src/client/directives/size.ts
index c4dd7b145d..541f38fd76 100644
--- a/src/client/directives/size.ts
+++ b/src/client/directives/size.ts
@@ -59,7 +59,7 @@ export default {
const ro = new ResizeObserver((entries, observer) => {
calc();
});
-
+
ro.observe(el);
el._ro_ = ro;
diff --git a/src/client/init.ts b/src/client/init.ts
index 2f2f9f5d59..29eabfee4e 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -81,14 +81,14 @@ if (lang == null) {
// Detect the user agent
const ua = navigator.userAgent.toLowerCase();
-let isMobile = /mobile|iphone|ipad|android/.test(ua);
+const isMobile = /mobile|iphone|ipad|android/.test(ua);
// Get the <head> element
const head = document.getElementsByTagName('head')[0];
// If mobile, insert the viewport meta tag
if (isMobile || window.innerWidth <= 1024) {
- const viewport = document.getElementsByName("viewport").item(0);
+ const viewport = document.getElementsByName('viewport').item(0);
viewport.setAttribute('content',
`${viewport.getAttribute('content')},minimum-scale=1,maximum-scale=1,user-scalable=no`);
head.appendChild(viewport);
diff --git a/src/client/mios.ts b/src/client/mios.ts
index a29dcd8550..aa2b202abd 100644
--- a/src/client/mios.ts
+++ b/src/client/mios.ts
@@ -123,8 +123,13 @@ export default class MiOS extends EventEmitter {
});
} else {
// Get token from localStorage
- const i = localStorage.getItem('i');
-
+ let i = localStorage.getItem('i');
+
+ // 連携ログインの場合用にCookieを参照する
+ if (i == null || i === 'null') {
+ i = (document.cookie.match(/igi=(\w+)/) || [null, null])[1];
+ }
+
fetchme(i, me => {
if (me) {
this.store.dispatch('login', me);
diff --git a/src/client/pages/auth.vue b/src/client/pages/auth.vue
index 8855948416..9f5b45f001 100644..100755
--- a/src/client/pages/auth.vue
+++ b/src/client/pages/auth.vue
@@ -26,7 +26,7 @@
</div>
<div class="signin" v-else>
<h1>{{ $t('sign-in') }}</h1>
- <mk-signin/>
+ <mk-signin @login="onLogin"/>
</div>
</template>
@@ -85,6 +85,9 @@ export default Vue.extend({
if (this.session.app.callbackUrl) {
location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
}
+ }, onLogin(res) {
+ localStorage.setItem('i', res.i);
+ location.reload();
}
}
});
diff --git a/src/client/pages/drive.vue b/src/client/pages/drive.vue
index 7b648939f2..8f8e949dcb 100644
--- a/src/client/pages/drive.vue
+++ b/src/client/pages/drive.vue
@@ -1,5 +1,5 @@
<template>
-<div>
+<div class="naked full">
<portal to="header">
<button @click="menu" class="_button _jmoebdiw_">
<fa :icon="faCloud" style="margin-right: 8px;"/>
diff --git a/src/client/pages/instance/queue.queue.vue b/src/client/pages/instance/queue.queue.vue
index 710f4ec099..7f0fc7d2bc 100644
--- a/src/client/pages/instance/queue.queue.vue
+++ b/src/client/pages/instance/queue.queue.vue
@@ -11,12 +11,12 @@
<canvas ref="chart"></canvas>
</div>
<div class="_content" style="max-height: 180px; overflow: auto;">
- <sequential-entrance :delay="15" v-if="jobs.length > 0">
- <div v-for="(job, i) in jobs" :key="job[0]">
+ <div v-if="jobs.length > 0">
+ <div v-for="job in jobs" :key="job[0]">
<span>{{ job[0] }}</span>
<span style="margin-left: 8px; opacity: 0.7;">({{ job[1] | number }} jobs)</span>
</div>
- </sequential-entrance>
+ </div>
<span v-else style="opacity: 0.5;">{{ $t('noJobs') }}</span>
</div>
</section>
diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue
index 7572108718..d11f840d8f 100644
--- a/src/client/pages/instance/settings.vue
+++ b/src/client/pages/instance/settings.vue
@@ -102,21 +102,20 @@
<div class="_content">
<mk-switch v-model="useObjectStorage">{{ $t('useObjectStorage') }}</mk-switch>
<template v-if="useObjectStorage">
- <mk-input v-model="objectStorageBaseUrl" :disabled="!useObjectStorage">URL</mk-input>
+ <mk-input v-model="objectStorageBaseUrl" :disabled="!useObjectStorage">{{ $t('objectStorageBaseUrl') }}<template #desc>{{ $t('objectStorageBaseUrlDesc') }}</template></mk-input>
<div class="_inputs">
- <mk-input v-model="objectStorageBucket" :disabled="!useObjectStorage">Bucket</mk-input>
- <mk-input v-model="objectStoragePrefix" :disabled="!useObjectStorage">Prefix</mk-input>
+ <mk-input v-model="objectStorageBucket" :disabled="!useObjectStorage">{{ $t('objectStorageBucket') }}<template #desc>{{ $t('objectStorageBucketDesc') }}</template></mk-input>
+ <mk-input v-model="objectStoragePrefix" :disabled="!useObjectStorage">{{ $t('objectStoragePrefix') }}<template #desc>{{ $t('objectStoragePrefixDesc') }}</template></mk-input>
</div>
- <mk-input v-model="objectStorageEndpoint" :disabled="!useObjectStorage">Endpoint</mk-input>
+ <mk-input v-model="objectStorageEndpoint" :disabled="!useObjectStorage">{{ $t('objectStorageEndpoint') }}<template #desc>{{ $t('objectStorageEndpointDesc') }}</template></mk-input>
<div class="_inputs">
- <mk-input v-model="objectStorageRegion" :disabled="!useObjectStorage">Region</mk-input>
- <mk-input v-model="objectStoragePort" type="number" :disabled="!useObjectStorage">Port</mk-input>
+ <mk-input v-model="objectStorageRegion" :disabled="!useObjectStorage">{{ $t('objectStorageRegion') }}<template #desc>{{ $t('objectStorageRegionDesc') }}</template></mk-input>
</div>
<div class="_inputs">
<mk-input v-model="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Access key</mk-input>
<mk-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Secret key</mk-input>
</div>
- <mk-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">SSL</mk-switch>
+ <mk-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('objectStorageUseSSL') }}<template #desc>{{ $t('objectStorageUseSSLDesc') }}</template></mk-switch>
</template>
</div>
<div class="_footer">
diff --git a/src/client/pages/messaging-room.message.vue b/src/client/pages/messaging-room.message.vue
index 48de2c7cd7..f26ef449b2 100644
--- a/src/client/pages/messaging-room.message.vue
+++ b/src/client/pages/messaging-room.message.vue
@@ -2,7 +2,7 @@
<div class="thvuemwp" :data-is-me="isMe">
<mk-avatar class="avatar" :user="message.user"/>
<div class="content">
- <div class="balloon _panel" :data-no-text="message.text == null">
+ <div class="balloon" :data-no-text="message.text == null">
<button class="delete-button" v-if="isMe" :title="$t('delete')" @click="del">
<img src="/assets/remove.png" alt="Delete"/>
</button>
@@ -243,13 +243,14 @@ export default Vue.extend({
}
&:not([data-is-me]) {
+ padding-left: var(--margin);
> .content {
padding-left: 16px;
padding-right: 32px;
> .balloon {
- $color: var(--panel);
+ $color: var(--messageBg);
background: $color;
&[data-no-text] {
@@ -279,6 +280,7 @@ export default Vue.extend({
&[data-is-me] {
flex-direction: row-reverse;
+ padding-right: var(--margin);
> .content {
padding-right: 16px;
@@ -287,7 +289,6 @@ export default Vue.extend({
> .balloon {
background: $me-balloon-color;
- box-shadow: 0 6px 16px var(--accentShadow);
text-align: left;
&[data-no-text] {
diff --git a/src/client/pages/messaging-room.vue b/src/client/pages/messaging-room.vue
index 7f7e77fc14..5fca8c0ff3 100644
--- a/src/client/pages/messaging-room.vue
+++ b/src/client/pages/messaging-room.vue
@@ -1,5 +1,5 @@
<template>
-<div class="mk-messaging-room"
+<div class="mk-messaging-room naked"
@dragover.prevent.stop="onDragover"
@drop.prevent.stop="onDrop"
>
diff --git a/src/client/pages/messaging.vue b/src/client/pages/messaging.vue
index bc85e7a56b..2179115dea 100644
--- a/src/client/pages/messaging.vue
+++ b/src/client/pages/messaging.vue
@@ -5,7 +5,7 @@
<mk-button @click="start" primary class="start"><fa :icon="faPlus"/> {{ $t('startMessaging') }}</mk-button>
- <sequential-entrance class="history" v-if="messages.length > 0" :delay="30">
+ <div class="history" v-if="messages.length > 0">
<router-link v-for="(message, i) in messages"
class="message _panel"
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
@@ -30,7 +30,7 @@
</div>
</div>
</router-link>
- </sequential-entrance>
+ </div>
<div class="no-history" v-if="!fetching && messages.length == 0">
<img src="https://xn--931a.moe/assets/info.png" class="_ghost"/>
<div>{{ $t('noHistory') }}</div>
diff --git a/src/client/pages/my-settings/integration.vue b/src/client/pages/my-settings/integration.vue
index 742d432018..3dd7783f12 100644
--- a/src/client/pages/my-settings/integration.vue
+++ b/src/client/pages/my-settings/integration.vue
@@ -70,11 +70,10 @@ export default Vue.extend({
},
mounted() {
- if (!document.cookie.match(/i=(\w+)/)) {
- document.cookie = `i=${this.$store.state.i.token}; path=/;` +
- ` domain=${document.location.hostname}; max-age=31536000;` +
+ document.cookie = `igi=${this.$store.state.i.token}; path=/;` +
+ ` max-age=31536000;` +
(document.location.protocol.startsWith('https') ? ' secure' : '');
- }
+
this.$watch('integrations', () => {
if (this.integrations.twitter) {
if (this.twitterForm) this.twitterForm.close();
diff --git a/src/client/pages/my-settings/reaction.vue b/src/client/pages/my-settings/reaction.vue
index 250769ec9e..b2df3f0231 100644
--- a/src/client/pages/my-settings/reaction.vue
+++ b/src/client/pages/my-settings/reaction.vue
@@ -2,7 +2,10 @@
<section class="_card">
<div class="_title"><fa :icon="faLaugh"/> {{ $t('reaction') }}</div>
<div class="_content">
- <mk-textarea v-model="reactions">{{ $t('reaction') }}<template #desc>{{ $t('reactionSettingDescription') }}</template></mk-textarea>
+ <mk-input v-model="reactions" style="font-family: 'Segoe UI Emoji', 'Noto Color Emoji', Roboto, HelveticaNeue, Arial, sans-serif">
+ {{ $t('reaction') }}<template #desc>{{ $t('reactionSettingDescription') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></template>
+ </mk-input>
+ <mk-button inline @click="setDefault"><fa :icon="faUndo"/> {{ $t('default') }}</mk-button>
</div>
<div class="_footer">
<mk-button @click="save()" primary inline :disabled="!changed"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
@@ -14,24 +17,26 @@
<script lang="ts">
import Vue from 'vue';
import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons';
-import MkTextarea from '../../components/ui/textarea.vue';
+import { faUndo } from '@fortawesome/free-solid-svg-icons';
+import MkInput from '../../components/ui/input.vue';
import MkButton from '../../components/ui/button.vue';
import MkReactionPicker from '../../components/reaction-picker.vue';
import i18n from '../../i18n';
+import { emojiRegexWithCustom } from '../../../misc/emoji-regex';
export default Vue.extend({
i18n,
components: {
- MkTextarea,
+ MkInput,
MkButton,
},
data() {
return {
- reactions: this.$store.state.settings.reactions.join('\n'),
+ reactions: this.$store.state.settings.reactions.join(''),
changed: false,
- faLaugh, faSave, faEye
+ faLaugh, faSave, faEye, faUndo
}
},
@@ -41,21 +46,40 @@ export default Vue.extend({
}
},
+ computed: {
+ splited(): any {
+ return this.reactions.match(emojiRegexWithCustom);
+ },
+ },
+
methods: {
save() {
- this.$store.dispatch('settings/set', { key: 'reactions', value: this.reactions.trim().split('\n') });
+ this.$store.dispatch('settings/set', { key: 'reactions', value: this.splited });
this.changed = false;
},
preview(ev) {
const picker = this.$root.new(MkReactionPicker, {
source: ev.currentTarget || ev.target,
- reactions: this.reactions.trim().split('\n'),
+ reactions: this.splited,
showFocus: false,
});
picker.$once('chosen', reaction => {
picker.close();
});
+ },
+
+ setDefault() {
+ this.reactions = '👍❤😆🤔😮🎉💢😥😇🍮';
+ },
+
+ async chooseEmoji(ev) {
+ const vm = this.$root.new(await import('../../components/emoji-picker.vue').then(m => m.default), {
+ source: ev.currentTarget || ev.target
+ }).$once('chosen', emoji => {
+ this.reactions += emoji;
+ vm.close();
+ });
}
}
});
diff --git a/src/client/pages/note.vue b/src/client/pages/note.vue
index 30741bab55..dbdf8c3d35 100644
--- a/src/client/pages/note.vue
+++ b/src/client/pages/note.vue
@@ -1,24 +1,28 @@
<template>
<div class="mk-note-page">
<portal to="avatar" v-if="note"><mk-avatar class="avatar" :user="note.user" :disable-preview="true"/></portal>
- <portal to="title" v-if="note">{{ $t('noteOf', { user: note.user.name }) }}</portal>
+ <portal to="title" v-if="note">
+ <mfm
+ :text="$t('noteOf', { user: note.user.name || note.user.username })"
+ :plain="true" :nowrap="true" :custom-emojis="note.user.emojis" :is-note="false"
+ />
+ </portal>
- <transition :name="$store.state.device.animation ? 'zoom' : ''" mode="out-in">
- <div v-if="note">
- <mk-button v-if="hasNext && !showNext" @click="showNext = true" primary style="margin: 0 auto var(--margin) auto;"><fa :icon="faChevronUp"/></mk-button>
- <x-notes v-if="showNext" ref="next" :pagination="next"/>
- <hr v-if="showNext"/>
+ <div v-if="note">
+ <button class="_panel _button" v-if="hasNext && !showNext" @click="showNext = true" style="margin: 0 auto var(--margin) auto;"><fa :icon="faChevronUp"/></button>
+ <x-notes v-if="showNext" ref="next" :pagination="next"/>
+ <hr v-if="showNext"/>
- <x-note :note="note" :key="note.id" :detail="true"/>
- <div v-if="error">
- <mk-error @retry="fetch()"/>
- </div>
-
- <mk-button v-if="hasPrev && !showPrev" @click="showPrev = true" primary style="margin: var(--margin) auto 0 auto;"><fa :icon="faChevronDown"/></mk-button>
- <hr v-if="showPrev"/>
- <x-notes v-if="showPrev" ref="prev" :pagination="prev" style="margin-top: var(--margin);"/>
+ <mk-remote-caution v-if="note.user.host != null" :href="note.uri" style="margin-bottom: var(--margin)"/>
+ <x-note :note="note" :key="note.id" :detail="true"/>
+ <div v-if="error">
+ <mk-error @retry="fetch()"/>
</div>
- </transition>
+
+ <button class="_panel _button" v-if="hasPrev && !showPrev" @click="showPrev = true" style="margin: var(--margin) auto 0 auto;"><fa :icon="faChevronDown"/></button>
+ <hr v-if="showPrev"/>
+ <x-notes v-if="showPrev" ref="prev" :pagination="prev" style="margin-top: var(--margin);"/>
+ </div>
</div>
</template>
@@ -29,7 +33,7 @@ import i18n from '../i18n';
import Progress from '../scripts/loading';
import XNote from '../components/note.vue';
import XNotes from '../components/notes.vue';
-import MkButton from '../components/ui/button.vue';
+import MkRemoteCaution from '../components/remote-caution.vue';
export default Vue.extend({
i18n,
@@ -41,7 +45,7 @@ export default Vue.extend({
components: {
XNote,
XNotes,
- MkButton,
+ MkRemoteCaution,
},
data() {
return {
diff --git a/src/client/pages/preferences/index.vue b/src/client/pages/preferences/index.vue
index f87f875107..8ffb481c99 100644
--- a/src/client/pages/preferences/index.vue
+++ b/src/client/pages/preferences/index.vue
@@ -8,8 +8,10 @@
<section class="_card">
<div class="_title"><fa :icon="faMusic"/> {{ $t('sounds') }}</div>
<div class="_content">
- {{ $t('volume') }}
- <input type="range" v-model="sfxVolume" min="0" max="1" step="0.1"/>
+ <mk-range v-model="sfxVolume" min="0" max="1" step="0.1">
+ <fa slot="icon" :icon="volumeIcon"/>
+ <span slot="title">{{ $t('volume') }}</span>
+ </mk-range>
</div>
<div class="_content">
<mk-select v-model="sfxNote">
@@ -61,7 +63,6 @@
<template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
</mk-switch>
<mk-switch v-model="showFixedPostForm">{{ $t('showFixedPostForm') }}</mk-switch>
- <mk-switch v-model="useNotificationsPopup">{{ $t('useNotificationsPopup') }}</mk-switch>
</div>
<div class="_content">
<mk-select v-model="lang">
@@ -85,12 +86,13 @@
<script lang="ts">
import Vue from 'vue';
-import { faImage, faCog, faMusic, faPlay } from '@fortawesome/free-solid-svg-icons';
+import { faImage, faCog, faMusic, faPlay, faVolumeUp, faVolumeMute } from '@fortawesome/free-solid-svg-icons';
import MkInput from '../../components/ui/input.vue';
import MkButton from '../../components/ui/button.vue';
import MkSwitch from '../../components/ui/switch.vue';
import MkSelect from '../../components/ui/select.vue';
import MkRadio from '../../components/ui/radio.vue';
+import MkRange from '../../components/ui/range.vue';
import XTheme from './theme.vue';
import i18n from '../../i18n';
import { langs } from '../../config';
@@ -128,6 +130,7 @@ export default Vue.extend({
MkSwitch,
MkSelect,
MkRadio,
+ MkRange
},
data() {
@@ -136,7 +139,7 @@ export default Vue.extend({
lang: localStorage.getItem('lang'),
fontSize: localStorage.getItem('fontSize'),
sounds,
- faImage, faCog, faMusic, faPlay
+ faImage, faCog, faMusic, faPlay, faVolumeUp, faVolumeMute
}
},
@@ -171,14 +174,9 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'showFixedPostForm', value }); }
},
- useNotificationsPopup: {
- get() { return this.$store.state.device.useNotificationsPopup; },
- set(value) { this.$store.commit('device/set', { key: 'useNotificationsPopup', value }); }
- },
-
sfxVolume: {
get() { return this.$store.state.device.sfxVolume; },
- set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value }); }
+ set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value: parseFloat(value, 10) }); }
},
sfxNote: {
@@ -210,6 +208,12 @@ export default Vue.extend({
get() { return this.$store.state.device.sfxAntenna; },
set(value) { this.$store.commit('device/set', { key: 'sfxAntenna', value }); }
},
+
+ volumeIcon: {
+ get() {
+ return this.sfxVolume === 0 ? faVolumeMute : faVolumeUp;
+ }
+ }
},
watch: {
diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue
index b5981937ed..9f5f968901 100644
--- a/src/client/pages/user/index.vue
+++ b/src/client/pages/user/index.vue
@@ -3,31 +3,13 @@
<portal to="title" v-if="user"><mk-user-name :user="user" :nowrap="false" class="name"/></portal>
<portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
- <div class="remote-caution _panel" v-if="user.host != null"><fa :icon="faExclamationTriangle" style="margin-right: 8px;"/>{{ $t('remoteUserCaution') }}<a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('showOnRemote') }}</a></div>
- <transition :name="$store.state.device.animation ? 'zoom' : ''" mode="out-in" appear>
- <div class="profile _panel" :key="user.id">
- <div class="banner-container" :style="style">
- <div class="banner" ref="banner" :style="style"></div>
- <div class="fade"></div>
- <div class="title">
- <mk-user-name class="name" :user="user" :nowrap="true"/>
- <div class="bottom">
- <span class="username"><mk-acct :user="user" :detail="true" /></span>
- <span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><fa :icon="faBookmark"/></span>
- <span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><fa :icon="farBookmark"/></span>
- <span v-if="user.isLocked" :title="$t('isLocked')"><fa :icon="faLock"/></span>
- <span v-if="user.isBot" :title="$t('isBot')"><fa :icon="faRobot"/></span>
- </div>
- </div>
- <span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span>
- <div class="actions" v-if="$store.getters.isSignedIn">
- <button @click="menu" class="menu _button" ref="menu"><fa :icon="faEllipsisH"/></button>
- <mk-follow-button v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
- </div>
- </div>
- <mk-avatar class="avatar" :user="user" :disable-preview="true"/>
+ <mk-remote-caution v-if="user.host != null" :href="user.url" style="margin-bottom: var(--margin)"/>
+ <div class="profile _panel" :key="user.id">
+ <div class="banner-container" :style="style">
+ <div class="banner" ref="banner" :style="style"></div>
+ <div class="fade"></div>
<div class="title">
- <mk-user-name :user="user" :nowrap="false" class="name"/>
+ <mk-user-name class="name" :user="user" :nowrap="true"/>
<div class="bottom">
<span class="username"><mk-acct :user="user" :detail="true" /></span>
<span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><fa :icon="faBookmark"/></span>
@@ -36,55 +18,71 @@
<span v-if="user.isBot" :title="$t('isBot')"><fa :icon="faRobot"/></span>
</div>
</div>
- <div class="description">
- <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
- <p v-else class="empty">{{ $t('noAccountDescription') }}</p>
- </div>
- <div class="fields system">
- <dl class="field" v-if="user.location">
- <dt class="name"><fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt>
- <dd class="value">{{ user.location }}</dd>
- </dl>
- <dl class="field" v-if="user.birthday">
- <dt class="name"><fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt>
- <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
- </dl>
- <dl class="field">
- <dt class="name"><fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt>
- <dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<mk-time :time="user.createdAt"/>)</dd>
- </dl>
+ <span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span>
+ <div class="actions" v-if="$store.getters.isSignedIn">
+ <button @click="menu" class="menu _button" ref="menu"><fa :icon="faEllipsisH"/></button>
+ <mk-follow-button v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
</div>
- <div class="fields" v-if="user.fields.length > 0">
- <dl class="field" v-for="(field, i) in user.fields" :key="i">
- <dt class="name">
- <mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
- </dt>
- <dd class="value">
- <mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/>
- </dd>
- </dl>
- </div>
- <div class="status" v-if="user.host === null">
- <router-link :to="user | userPage()" :class="{ active: $route.name === 'user' }">
- <b>{{ user.notesCount | number }}</b>
- <span>{{ $t('notes') }}</span>
- </router-link>
- <router-link :to="user | userPage('following')" :class="{ active: $route.name === 'userFollowing' }">
- <b>{{ user.followingCount | number }}</b>
- <span>{{ $t('following') }}</span>
- </router-link>
- <router-link :to="user | userPage('followers')" :class="{ active: $route.name === 'userFollowers' }">
- <b>{{ user.followersCount | number }}</b>
- <span>{{ $t('followers') }}</span>
- </router-link>
+ </div>
+ <mk-avatar class="avatar" :user="user" :disable-preview="true"/>
+ <div class="title">
+ <mk-user-name :user="user" :nowrap="false" class="name"/>
+ <div class="bottom">
+ <span class="username"><mk-acct :user="user" :detail="true" /></span>
+ <span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><fa :icon="faBookmark"/></span>
+ <span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><fa :icon="farBookmark"/></span>
+ <span v-if="user.isLocked" :title="$t('isLocked')"><fa :icon="faLock"/></span>
+ <span v-if="user.isBot" :title="$t('isBot')"><fa :icon="faRobot"/></span>
</div>
</div>
- </transition>
+ <div class="description">
+ <mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
+ <p v-else class="empty">{{ $t('noAccountDescription') }}</p>
+ </div>
+ <div class="fields system">
+ <dl class="field" v-if="user.location">
+ <dt class="name"><fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt>
+ <dd class="value">{{ user.location }}</dd>
+ </dl>
+ <dl class="field" v-if="user.birthday">
+ <dt class="name"><fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt>
+ <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
+ </dl>
+ <dl class="field">
+ <dt class="name"><fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt>
+ <dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<mk-time :time="user.createdAt"/>)</dd>
+ </dl>
+ </div>
+ <div class="fields" v-if="user.fields.length > 0">
+ <dl class="field" v-for="(field, i) in user.fields" :key="i">
+ <dt class="name">
+ <mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
+ </dt>
+ <dd class="value">
+ <mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/>
+ </dd>
+ </dl>
+ </div>
+ <div class="status">
+ <router-link :to="user | userPage()" :class="{ active: $route.name === 'user' }">
+ <b>{{ user.notesCount | number }}</b>
+ <span>{{ $t('notes') }}</span>
+ </router-link>
+ <router-link :to="user | userPage('following')" :class="{ active: $route.name === 'userFollowing' }">
+ <b>{{ user.followingCount | number }}</b>
+ <span>{{ $t('following') }}</span>
+ </router-link>
+ <router-link :to="user | userPage('followers')" :class="{ active: $route.name === 'userFollowers' }">
+ <b>{{ user.followersCount | number }}</b>
+ <span>{{ $t('followers') }}</span>
+ </router-link>
+ </div>
+ </div>
<router-view :user="user"></router-view>
<template v-if="$route.name == 'user'">
- <sequential-entrance class="pins">
- <x-note v-for="(note, i) in user.pinnedNotes" class="note" :note="note" :key="note.id" :detail="true" :pinned="true"/>
- </sequential-entrance>
+ <div class="pins">
+ <x-note v-for="note in user.pinnedNotes" class="note" :note="note" :key="note.id" :detail="true" :pinned="true"/>
+ </div>
<mk-container :body-togglable="true" class="content">
<template #header><fa :icon="faImage"/>{{ $t('images') }}</template>
<div>
@@ -107,7 +105,7 @@
<script lang="ts">
import Vue from 'vue';
-import { faEllipsisH, faRobot, faLock, faBookmark, faExclamationTriangle, faChartBar, faImage, faBirthdayCake, faMapMarker } from '@fortawesome/free-solid-svg-icons';
+import { faEllipsisH, faRobot, faLock, faBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker } from '@fortawesome/free-solid-svg-icons';
import { faCalendarAlt, faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons';
import * as age from 's-age';
import XUserTimeline from './index.timeline.vue';
@@ -115,6 +113,7 @@ import XUserMenu from '../../components/user-menu.vue';
import XNote from '../../components/note.vue';
import MkFollowButton from '../../components/follow-button.vue';
import MkContainer from '../../components/ui/container.vue';
+import MkRemoteCaution from '../../components/remote-caution.vue';
import Progress from '../../scripts/loading';
import parseAcct from '../../../misc/acct/parse';
@@ -124,6 +123,7 @@ export default Vue.extend({
XNote,
MkFollowButton,
MkContainer,
+ MkRemoteCaution,
XPhotos: () => import('./index.photos.vue').then(m => m.default),
XActivity: () => import('./index.activity.vue').then(m => m.default),
},
@@ -139,7 +139,7 @@ export default Vue.extend({
user: null,
error: null,
parallaxAnimationId: null,
- faEllipsisH, faRobot, faLock, faBookmark, farBookmark, faExclamationTriangle, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt
+ faEllipsisH, faRobot, faLock, faBookmark, farBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt
};
},
@@ -217,17 +217,6 @@ export default Vue.extend({
<style lang="scss" scoped>
.mk-user-page {
- > .remote-caution {
- font-size: 0.8em;
- padding: 16px;
- margin-bottom: var(--margin);
-
- > a {
- margin-left: 4px;
- color: var(--accent);
- }
- }
-
> .profile {
position: relative;
margin-bottom: var(--margin);
diff --git a/src/client/scripts/hotkey.ts b/src/client/scripts/hotkey.ts
index ec627ab15b..672dbedde1 100644
--- a/src/client/scripts/hotkey.ts
+++ b/src/client/scripts/hotkey.ts
@@ -12,14 +12,22 @@ type action = {
patterns: pattern[];
callback: Function;
+
+ allowRepeat: boolean;
};
const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => {
const result = {
patterns: [],
- callback: callback
+ callback: callback,
+ allowRepeat: true
} as action;
+ if (patterns.match(/^\(.*\)$/) !== null) {
+ result.allowRepeat = false;
+ patterns = patterns.slice(1, -1);
+ }
+
result.patterns = patterns.split('|').map(part => {
const pattern = {
which: [],
@@ -77,6 +85,7 @@ export default {
const matched = match(e, action.patterns);
if (matched) {
+ if (!action.allowRepeat && e.repeat) return;
if (el._hotkey_global && match(e, targetReservedKeys)) return;
e.preventDefault();
diff --git a/src/client/store.ts b/src/client/store.ts
index 3064cfdec7..29709096ee 100644
--- a/src/client/store.ts
+++ b/src/client/store.ts
@@ -40,7 +40,6 @@ const defaultDeviceSettings = {
animatedMfm: true,
imageNewTab: false,
showFixedPostForm: false,
- useNotificationsPopup: true,
sfxVolume: 0.3,
sfxNote: 'syuilo/down',
sfxNoteMy: 'syuilo/up',
@@ -101,6 +100,7 @@ export default (os: MiOS) => new Vuex.Store({
ctx.commit('settings/init', {});
ctx.commit('deviceUser/init', {});
localStorage.removeItem('i');
+ document.cookie = `igi=; path=/`;
},
async switchAccount(ctx, i) {
diff --git a/src/client/style.scss b/src/client/style.scss
index 93d4159d4d..fee64c7ca8 100644
--- a/src/client/style.scss
+++ b/src/client/style.scss
@@ -3,7 +3,7 @@
:root {
--radius: 8px;
--marginFull: 16px;
- --marginHalf: 8px;
+ --marginHalf: 10px;
--margin: var(--marginFull);
@@ -230,7 +230,6 @@ hr {
@extend ._button;
color: #fff;
background: var(--accent);
- box-shadow: 0 6px 16px var(--accentShadow);
&:not(:disabled):hover {
background: var(--jkhztclx);
@@ -276,23 +275,29 @@ hr {
}
}
-._shadow {
- box-shadow: 0 8px 32px var(--shadow);
-
- @media (max-width: 700px) {
- box-shadow: 0 4px 16px var(--shadow);
- }
-
- @media (max-width: 500px) {
- box-shadow: 0 2px 8px var(--shadow);
- }
-}
-
._panel {
- @extend ._shadow;
position: relative;
background: var(--panel);
border-radius: var(--radius);
+ box-shadow: 0 0 0 1px var(--divider);
+}
+
+main ._panel {
+ border-radius: 0;
+ box-shadow: 0 1px 0 0 var(--divider), 0 -1px 0 0 var(--divider);
+}
+
+._panel ._panel {
+ border-radius: 0;
+ box-shadow: 0 1px 0 0 var(--divider), 0 -1px 0 0 var(--divider);
+}
+
+._panel._button {
+ display: flex;
+ width: 100%;
+ min-height: 48px;
+ align-items: center;
+ justify-content: center;
}
._card {
diff --git a/src/client/themes/_dark.json5 b/src/client/themes/_dark.json5
index 5f30d2141b..bc7c0efc09 100644
--- a/src/client/themes/_dark.json5
+++ b/src/client/themes/_dark.json5
@@ -17,9 +17,10 @@
fgHighlighted: ':lighten<3<@fg',
html: '@bg',
indicator: '@accent',
- panel: '#111213',
+ panel: '#000',
shadow: 'rgba(0, 0, 0, 0.1)',
header: 'rgba(20, 20, 20, 0.75)',
+ pageBg: ':lighten<5<@bg',
navBg: '@panel',
navFg: '@fg',
navHoverFg: ':lighten<17<@fg',
@@ -33,8 +34,7 @@
divider: 'rgba(255, 255, 255, 0.1)',
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
- dateLabelBg: 'rgba(255, 255, 255, 0.08)',
- dateLabelFg: '#fff',
+ dateLabelFg: '@fg',
infoBg: '#253142',
infoFg: '#fff',
infoWarnBg: '#42321c',
@@ -51,14 +51,13 @@
driveFolderBg: ':alpha<0.3<@accent',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
badge: '#31b1ce',
+ messageBg: ':lighten<5<@bg',
bonzsgfz: ':alpha<0<@bg',
pcncwizz: ':darken<2<@panel',
- vocsgcxy: 'rgba(0, 0, 0, 0.5)',
yrnqrguo: 'rgba(255, 255, 255, 0.05)',
nwjktjjq: 'rgba(255, 255, 255, 0.1)',
geavgsxy: 'rgba(255, 255, 255, 0.05)',
nhzhphzx: 'rgba(255, 255, 255, 0.15)',
- tyvedwbe: 'rgba(0, 0, 0, 0.5)',
bwqtlupy: 'rgba(255, 255, 255, 0.05)',
jkhztclx: ':lighten<5<@accent',
zbqjwygh: ':darken<5<@accent',
diff --git a/src/client/themes/_light.json5 b/src/client/themes/_light.json5
index 2b411fb28d..adb1280420 100644
--- a/src/client/themes/_light.json5
+++ b/src/client/themes/_light.json5
@@ -20,6 +20,7 @@
panel: '#fff',
shadow: 'rgba(0, 0, 0, 0.1)',
header: 'rgba(255, 255, 255, 0.75)',
+ pageBg: '@bg',
navBg: '@panel',
navFg: '@fg',
navHoverFg: ':darken<17<@fg',
@@ -33,8 +34,7 @@
divider: 'rgba(0, 0, 0, 0.1)',
scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
- dateLabelBg: 'rgba(0, 0, 0, 0.5)',
- dateLabelFg: '#fff',
+ dateLabelFg: '@fg',
infoBg: '#e5f5ff',
infoFg: '#72818a',
infoWarnBg: '#fff0db',
@@ -51,14 +51,13 @@
driveFolderBg: ':alpha<0.3<@accent',
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
badge: '#31b1ce',
+ messageBg: '@panel',
bonzsgfz: ':alpha<0<@bg',
pcncwizz: ':darken<2<@panel',
- vocsgcxy: 'rgba(255, 255, 255, 0.5)',
yrnqrguo: 'rgba(0, 0, 0, 0.05)',
nwjktjjq: 'rgba(0, 0, 0, 0.1)',
geavgsxy: 'rgba(0, 0, 0, 0.05)',
nhzhphzx: 'rgba(0, 0, 0, 0.25)',
- tyvedwbe: 'rgba(0, 0, 0, 0.1)',
bwqtlupy: 'rgba(0, 0, 0, 0.05)',
jkhztclx: ':lighten<5<@accent',
zbqjwygh: ':darken<5<@accent',
diff --git a/src/client/themes/lavender.json5 b/src/client/themes/lavender.json5
index 4eb4a54749..faa4093612 100644
--- a/src/client/themes/lavender.json5
+++ b/src/client/themes/lavender.json5
@@ -14,6 +14,5 @@
link: '@accent',
mention: '@accent',
hashtag: '@accent',
- dateLabelBg: 'rgb(204, 186, 188)',
},
}
diff --git a/src/client/widgets/notifications.vue b/src/client/widgets/notifications.vue
index 2a718a6666..9c1bddb2ee 100644
--- a/src/client/widgets/notifications.vue
+++ b/src/client/widgets/notifications.vue
@@ -3,7 +3,7 @@
<mk-container :show-header="!props.compact" class="container">
<template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template>
- <div class="tl">
+ <div>
<x-notifications/>
</div>
</mk-container>
@@ -81,10 +81,5 @@ export default define({
flex-grow: 1;
}
}
-
- .tl {
- background: var(--bg);
- padding: 8px;
- }
}
</style>
diff --git a/src/client/widgets/timeline.vue b/src/client/widgets/timeline.vue
index ab5664a4d8..55f78f985f 100644
--- a/src/client/widgets/timeline.vue
+++ b/src/client/widgets/timeline.vue
@@ -14,7 +14,7 @@
</button>
</template>
- <div class="tl">
+ <div>
<x-timeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list" :antenna="props.antenna"/>
</div>
</mk-container>
@@ -148,11 +148,5 @@ export default define({
flex-grow: 1;
}
}
-
- .tl {
- padding: 8px;
- background: var(--bg);
- box-sizing: border-box;
- }
}
</style>
diff --git a/src/config/types.ts b/src/config/types.ts
index 78ae025133..a33901bde6 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -27,9 +27,10 @@ export type Source = {
elasticsearch: {
host: string;
port: number;
- pass: string;
- index?: string;
ssl?: boolean;
+ user?: string;
+ pass?: string;
+ index?: string;
};
proxy?: string;
diff --git a/src/db/elasticsearch.ts b/src/db/elasticsearch.ts
index b62e17461a..048e399bdf 100644
--- a/src/db/elasticsearch.ts
+++ b/src/db/elasticsearch.ts
@@ -33,6 +33,10 @@ const index = {
// Init ElasticSearch connection
const client = config.elasticsearch ? new elasticsearch.Client({
node: `${config.elasticsearch.ssl ? 'https://' : 'http://'}${config.elasticsearch.host}:${config.elasticsearch.port}`,
+ auth: (config.elasticsearch.user && config.elasticsearch.pass) ? {
+ username: config.elasticsearch.user,
+ password: config.elasticsearch.pass
+ } : undefined,
pingTimeout: 30000
}) : null;
diff --git a/src/mfm/toString.ts b/src/mfm/toString.ts
new file mode 100644
index 0000000000..5b53105b32
--- /dev/null
+++ b/src/mfm/toString.ts
@@ -0,0 +1,112 @@
+import { MfmForest, MfmTree } from './prelude';
+import { nyaize } from '../misc/nyaize';
+
+export type RestoreOptions = {
+ doNyaize?: boolean;
+};
+
+export function toString(tokens: MfmForest | null, opts?: RestoreOptions): string {
+
+ if (tokens === null) return '';
+
+ function appendChildren(children: MfmForest, opts?: RestoreOptions): string {
+ return children.map(t => handlers[t.node.type](t, opts)).join('');
+ }
+
+ const handlers: { [key: string]: (token: MfmTree, opts?: RestoreOptions) => string } = {
+ bold(token, opts) {
+ return `**${appendChildren(token.children, opts)}**`;
+ },
+
+ big(token, opts) {
+ return `***${appendChildren(token.children, opts)}***`;
+ },
+
+ small(token, opts) {
+ return `<small>${appendChildren(token.children, opts)}</small>`;
+ },
+
+ strike(token, opts) {
+ return `~~${appendChildren(token.children, opts)}~~`;
+ },
+
+ italic(token, opts) {
+ return `<i>${appendChildren(token.children, opts)}</i>`;
+ },
+
+ motion(token, opts) {
+ return `<motion>${appendChildren(token.children, opts)}</motion>`;
+ },
+
+ spin(token, opts) {
+ return `<spin>${appendChildren(token.children, opts)}</spin>`;
+ },
+
+ jump(token, opts) {
+ return `<jump>${appendChildren(token.children, opts)}</jump>`;
+ },
+
+ flip(token, opts) {
+ return `<flip>${appendChildren(token.children, opts)}</flip>`;
+ },
+
+ blockCode(token) {
+ return `\`\`\`${token.node.props.lang || ''}\n${token.node.props.code}\n\`\`\`\n`;
+ },
+
+ center(token, opts) {
+ return `<center>${appendChildren(token.children, opts)}</center>`;
+ },
+
+ emoji(token) {
+ return (token.node.props.emoji ? token.node.props.emoji : `:${token.node.props.name}:`);
+ },
+
+ hashtag(token) {
+ return `#${token.node.props.hashtag}`;
+ },
+
+ inlineCode(token) {
+ return `\`${token.node.props.code}\``;
+ },
+
+ mathInline(token) {
+ return `\\(${token.node.props.formula}\\)`;
+ },
+
+ mathBlock(token) {
+ return `\\[${token.node.props.formula}\\]`;
+ },
+
+ link(token, opts) {
+ return `[${appendChildren(token.children, opts)}](${token.node.props.url})`;
+ },
+
+ mention(token) {
+ return token.node.props.canonical;
+ },
+
+ quote(token) {
+ return `${appendChildren(token.children, {doNyaize: false}).replace(/^/gm,'>').trim()}\n`;
+ },
+
+ title(token, opts) {
+ return `[${appendChildren(token.children, opts)}]\n`;
+ },
+
+ text(token, opts) {
+ return (opts && opts.doNyaize) ? nyaize(token.node.props.text) : token.node.props.text;
+ },
+
+ url(token) {
+ return `<${token.node.props.url}>`;
+ },
+
+ search(token, opts) {
+ const query = token.node.props.query;
+ return `${(opts && opts.doNyaize ? nyaize(query) : query)} [search]\n`;
+ }
+ };
+
+ return appendChildren(tokens, { doNyaize: (opts && opts.doNyaize) || false }).trim();
+}
diff --git a/src/misc/app-lock.ts b/src/misc/app-lock.ts
index 3d5ff91882..ca2181f879 100644
--- a/src/misc/app-lock.ts
+++ b/src/misc/app-lock.ts
@@ -24,3 +24,7 @@ export function getApLock(uri: string, timeout = 30 * 1000) {
export function getNodeinfoLock(host: string, timeout = 30 * 1000) {
return lock(`nodeinfo:${host}`, timeout);
}
+
+export function getChartInsertLock(lockKey: string, timeout = 30 * 1000) {
+ return lock(`chart-insert:${lockKey}`, timeout);
+}
diff --git a/src/misc/check-hit-antenna.ts b/src/misc/check-hit-antenna.ts
index 0d72c3f340..fa24794984 100644
--- a/src/misc/check-hit-antenna.ts
+++ b/src/misc/check-hit-antenna.ts
@@ -48,7 +48,7 @@ export async function checkHitAntenna(antenna: Antenna, note: Note, noteUser: Us
? note.text!.includes(keyword)
: note.text!.toLowerCase().includes(keyword.toLowerCase())
));
-
+
if (!matched) return false;
}
@@ -61,7 +61,7 @@ export async function checkHitAntenna(antenna: Antenna, note: Note, noteUser: Us
? note.text!.includes(keyword)
: note.text!.toLowerCase().includes(keyword.toLowerCase())
));
-
+
if (matched) return false;
}
diff --git a/src/misc/count-same-renotes.ts b/src/misc/count-same-renotes.ts
new file mode 100644
index 0000000000..0233bdf88e
--- /dev/null
+++ b/src/misc/count-same-renotes.ts
@@ -0,0 +1,15 @@
+import { Notes } from '../models';
+
+export async function countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
+ // 指定したユーザーの指定したノートのリノートがいくつあるか数える
+ const query = Notes.createQueryBuilder('note')
+ .where('note.userId = :userId', { userId })
+ .andWhere('note.renoteId = :renoteId', { renoteId })
+
+ // 指定した投稿を除く
+ if (excludeNoteId) {
+ query.andWhere('note.id != :excludeNoteId', { excludeNoteId })
+ }
+
+ return await query.getCount();
+}
diff --git a/src/misc/emoji-regex.ts b/src/misc/emoji-regex.ts
index 9e4b67ed08..7a0861c92b 100644
--- a/src/misc/emoji-regex.ts
+++ b/src/misc/emoji-regex.ts
@@ -1,2 +1,4 @@
// https://github.com/twitter/twemoji-parser/blob/master/src/lib/regex.js @12.1.3
export const emojiRegex = /((?:\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d])|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5\udeeb\udeec\udef4-\udefa\udfe0-\udfeb]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd1d\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd71\udd73-\udd76\udd7a-\udda2\udda5-\uddaa\uddae-\uddb4\uddb7\uddba\uddbc-\uddca\uddd0\uddde-\uddff\ude70-\ude73\ude78-\ude7a\ude80-\ude82\ude90-\ude95]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f)/;
+
+export const emojiRegexWithCustom = new RegExp(`(${emojiRegex.source}|:[0-9A-Za-z_]+:)`, 'g');
diff --git a/src/misc/nyaize.ts b/src/misc/nyaize.ts
index 9fbfc8b500..6ee3b68477 100644
--- a/src/misc/nyaize.ts
+++ b/src/misc/nyaize.ts
@@ -1,8 +1,5 @@
-import rndstr from 'rndstr';
-
export function nyaize(text: string): string {
- const [toNyaize, exclusionMap] = exclude(text);
- const nyaized = toNyaize
+ return text
// ja-JP
.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ')
// en-US
@@ -13,34 +10,4 @@ export function nyaize(text: string): string {
))
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');
- return replaceExceptions(nyaized, exclusionMap);
-}
-
-function exclude(text: string): [string, Record<string, string>] {
- const map: Record<string, string> = {};
- function substitute(match: string): string {
- let randomstr: string;
- do {
- randomstr = rndstr({ length: 16, chars: '🀀-🀫' });
- } while(Object.prototype.hasOwnProperty.call(map, randomstr));
- map[randomstr] = match;
- return randomstr;
- }
- const replaced = text
- .replace(/```(.+?)?\n([\s\S]+?)```(\n|$)/gm, match => substitute(match)) // code block
- .replace(/`([^`\n]+?)`/g, match => substitute(match)) // inline code
- .replace(/(https?:\/\/.*?)(?= |$)/gm, match => substitute(match)) // URL
- .replace(/:([a-z0-9_+-]+):/gim, match => substitute(match)) // emoji
- .replace(/#([^\s.,!?'"#:\/\[\]【】]+)/gm, match => substitute(match)) // hashtag
- .replace(/@\w([\w-]*\w)?(?:@[\w.\-]+\w)?/gm, match => substitute(match)); // mention
- return [replaced, map];
-}
-
-function replaceExceptions(text: string, map: Record<string, string>): string {
- for (const rule in map) {
- if (Object.prototype.hasOwnProperty.call(map, rule)) {
- text = text.replace(rule, map[rule]);
- }
- }
- return text;
}
diff --git a/src/models/entities/clip-note.ts b/src/models/entities/clip-note.ts
index 19e4750fc6..7d96b2ef7a 100644
--- a/src/models/entities/clip-note.ts
+++ b/src/models/entities/clip-note.ts
@@ -8,7 +8,7 @@ import { id } from '../id';
export class ClipNote {
@PrimaryColumn(id())
public id: string;
-
+
@Index()
@Column({
...id(),
diff --git a/src/models/entities/note-reaction.ts b/src/models/entities/note-reaction.ts
index a958e89570..995748760c 100644
--- a/src/models/entities/note-reaction.ts
+++ b/src/models/entities/note-reaction.ts
@@ -36,7 +36,7 @@ export class NoteReaction {
public note: Note | null;
@Column('varchar', {
- length: 128
+ length: 130
})
public reaction: string;
}
diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts
index cc856f2ba1..73d7ad86eb 100644
--- a/src/models/repositories/note.ts
+++ b/src/models/repositories/note.ts
@@ -1,12 +1,13 @@
import { EntityRepository, Repository, In } from 'typeorm';
import { Note } from '../entities/note';
import { User } from '../entities/user';
-import { nyaize } from '../../misc/nyaize';
import { Emojis, Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls } from '..';
import { ensure } from '../../prelude/ensure';
import { SchemaType } from '../../misc/schema';
import { awaitAll } from '../../prelude/await-all';
import { convertLegacyReaction, convertLegacyReactions } from '../../misc/reaction-lib';
+import { toString } from '../../mfm/toString';
+import { parse } from '../../mfm/parse';
export type PackedNote = SchemaType<typeof packedNoteSchema>;
@@ -217,7 +218,8 @@ export class NoteRepository extends Repository<Note> {
});
if (packed.user.isCat && packed.text) {
- packed.text = nyaize(packed.text);
+ const tokens = packed.text ? parse(packed.text) : [];
+ packed.text = toString(tokens, { doNyaize: true });
}
if (!opts.skipHide) {
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
index 1d669feb5e..c6bc35030c 100644
--- a/src/models/repositories/user.ts
+++ b/src/models/repositories/user.ts
@@ -98,7 +98,7 @@ export class UserRepository extends Repository<User> {
public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
const antennas = await Antennas.find({ userId });
-
+
const unread = antennas.length > 0 ? await AntennaNotes.findOne({
antennaId: In(antennas.map(x => x.id)),
read: false
@@ -112,7 +112,7 @@ export class UserRepository extends Repository<User> {
muterId: userId
});
const mutedUserIds = mute.map(m => m.muteeId);
-
+
const count = await Notifications.count({
where: {
notifieeId: userId,
diff --git a/src/prelude/time.ts b/src/prelude/time.ts
index b1824b42ee..a65366d74a 100644
--- a/src/prelude/time.ts
+++ b/src/prelude/time.ts
@@ -1,17 +1,15 @@
const dateTimeIntervals = {
- 'days': 86400000,
- 'hours': 3600000,
+ 'day': 86400000,
+ 'hour': 3600000,
+ 'ms': 1,
};
-export function DateUTC(time: number[]): Date {
- const r = new Date(0);
- r.setUTCFullYear(time[0], time[1], time[2]);
- if (time[3]) r.setUTCHours(time[3], ...time.slice(4));
- return r;
+export function dateUTC(time: number[]): Date {
+ return new Date(Date.UTC(...time));
}
export function isTimeSame(a: Date, b: Date): boolean {
- return (a.getTime() - b.getTime()) === 0;
+ return a.getTime() === b.getTime();
}
export function isTimeBefore(a: Date, b: Date): boolean {
@@ -22,10 +20,10 @@ export function isTimeAfter(a: Date, b: Date): boolean {
return (a.getTime() - b.getTime()) > 0;
}
-export function addTimespan(x: Date, value: number, span: keyof typeof dateTimeIntervals): Date {
+export function addTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date {
return new Date(x.getTime() + (value * dateTimeIntervals[span]));
}
-export function subtractTimespan(x: Date, value: number, span: keyof typeof dateTimeIntervals): Date {
+export function subtractTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date {
return new Date(x.getTime() - (value * dateTimeIntervals[span]));
}
diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts
index dc777a3c5d..0f87381a44 100644
--- a/src/remote/activitypub/request.ts
+++ b/src/remote/activitypub/request.ts
@@ -7,10 +7,10 @@ import config from '../../config';
import { ILocalUser } from '../../models/entities/user';
import { UserKeypairs } from '../../models';
import { ensure } from '../../prelude/ensure';
-import * as httpsProxyAgent from 'https-proxy-agent';
+import { HttpsProxyAgent } from 'https-proxy-agent';
const agent = config.proxy
- ? new httpsProxyAgent(config.proxy)
+ ? new HttpsProxyAgent(config.proxy)
: new https.Agent({
lookup: cache.lookup,
});
diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts
index aa2786f8fc..50f79f1919 100644
--- a/src/server/api/common/signin.ts
+++ b/src/server/api/common/signin.ts
@@ -9,16 +9,12 @@ import { publishMainStream } from '../../../services/stream';
export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
if (redirect) {
//#region Cookie
- const expires = 1000 * 60 * 60 * 24 * 365; // One Year
- ctx.cookies.set('i', user.token, {
+ ctx.cookies.set('igi', user.token, {
path: '/',
- domain: config.hostname,
// SEE: https://github.com/koajs/koa/issues/974
// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
secure: config.url.startsWith('https'),
- httpOnly: false,
- expires: new Date(Date.now() + expires),
- maxAge: expires
+ httpOnly: false
});
//#endregion
diff --git a/src/server/api/endpoints/antennas/create.ts b/src/server/api/endpoints/antennas/create.ts
index f11b198f86..bc79385260 100644
--- a/src/server/api/endpoints/antennas/create.ts
+++ b/src/server/api/endpoints/antennas/create.ts
@@ -82,7 +82,7 @@ export default define(meta, async (ps, user) => {
id: ps.userListId,
userId: user.id,
});
-
+
if (userList == null) {
throw new ApiError(meta.errors.noSuchUserList);
}
@@ -91,7 +91,7 @@ export default define(meta, async (ps, user) => {
userGroupId: ps.userGroupId,
userId: user.id,
});
-
+
if (userGroupJoining == null) {
throw new ApiError(meta.errors.noSuchUserGroup);
}
diff --git a/src/server/api/endpoints/antennas/update.ts b/src/server/api/endpoints/antennas/update.ts
index ab4ce57937..b329e86ade 100644
--- a/src/server/api/endpoints/antennas/update.ts
+++ b/src/server/api/endpoints/antennas/update.ts
@@ -101,7 +101,7 @@ export default define(meta, async (ps, user) => {
id: ps.userListId,
userId: user.id,
});
-
+
if (userList == null) {
throw new ApiError(meta.errors.noSuchUserList);
}
@@ -110,7 +110,7 @@ export default define(meta, async (ps, user) => {
userGroupId: ps.userGroupId,
userId: user.id,
});
-
+
if (userGroupJoining == null) {
throw new ApiError(meta.errors.noSuchUserGroup);
}
diff --git a/src/server/api/endpoints/charts/active-users.ts b/src/server/api/endpoints/charts/active-users.ts
index 59bb1db109..df427ff4b7 100644
--- a/src/server/api/endpoints/charts/active-users.ts
+++ b/src/server/api/endpoints/charts/active-users.ts
@@ -25,11 +25,16 @@ export const meta = {
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
}
},
+
+ offset: {
+ validator: $.optional.nullable.num,
+ default: null,
+ },
},
res: convertLog(activeUsersChart.schema),
};
export default define(meta, async (ps) => {
- return await activeUsersChart.getChart(ps.span as any, ps.limit!);
+ return await activeUsersChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
});
diff --git a/src/server/api/endpoints/charts/drive.ts b/src/server/api/endpoints/charts/drive.ts
index 5c26fe719a..e1f279fa0a 100644
--- a/src/server/api/endpoints/charts/drive.ts
+++ b/src/server/api/endpoints/charts/drive.ts
@@ -25,11 +25,16 @@ export const meta = {
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
}
},
+
+ offset: {
+ validator: $.optional.nullable.num,
+ default: null,
+ },
},
res: convertLog(driveChart.schema),
};
export default define(meta, async (ps) => {
- return await driveChart.getChart(ps.span as any, ps.limit!);
+ return await driveChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
});
diff --git a/src/server/api/endpoints/charts/federation.ts b/src/server/api/endpoints/charts/federation.ts
index ebd60cc24b..581e42f307 100644
--- a/src/server/api/endpoints/charts/federation.ts
+++ b/src/server/api/endpoints/charts/federation.ts
@@ -25,11 +25,16 @@ export const meta = {
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
}
},
+
+ offset: {
+ validator: $.optional.nullable.num,
+ default: null,
+ },
},
res: convertLog(federationChart.schema),
};
export default define(meta, async (ps) => {
- return await federationChart.getChart(ps.span as any, ps.limit!);
+ return await federationChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
});
diff --git a/src/server/api/endpoints/charts/hashtag.ts b/src/server/api/endpoints/charts/hashtag.ts
index 8d14430137..1aa5c86b35 100644
--- a/src/server/api/endpoints/charts/hashtag.ts
+++ b/src/server/api/endpoints/charts/hashtag.ts
@@ -26,6 +26,11 @@ export const meta = {
}
},
+ offset: {
+ validator: $.optional.nullable.num,
+ default: null,
+ },
+
tag: {
validator: $.str,
desc: {
@@ -38,5 +43,5 @@ export const meta = {
};
export default define(meta, async (ps) => {
- return await hashtagChart.getChart(ps.span as any, ps.limit!, ps.tag);
+ return await hashtagChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.tag);
});
diff --git a/src/server/api/endpoints/charts/instance.ts b/src/server/api/endpoints/charts/instance.ts
index 4c26b7614c..f0f85ed71a 100644
--- a/src/server/api/endpoints/charts/instance.ts
+++ b/src/server/api/endpoints/charts/instance.ts
@@ -26,6 +26,11 @@ export const meta = {
}
},
+ offset: {
+ validator: $.optional.nullable.num,
+ default: null,
+ },
+
host: {
validator: $.str,
desc: {
@@ -39,5 +44,5 @@ export const meta = {
};
export default define(meta, async (ps) => {
- return await instanceChart.getChart(ps.span as any, ps.limit!, ps.host);
+ return await instanceChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.host);
});
diff --git a/src/server/api/endpoints/charts/network.ts b/src/server/api/endpoints/charts/network.ts
index 162c0c9ecd..d1337681a9 100644
--- a/src/server/api/endpoints/charts/network.ts
+++ b/src/server/api/endpoints/charts/network.ts
@@ -25,11 +25,16 @@ export const meta = {
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
}
},
+
+ offset: {
+ validator: $.optional.nullable.num,
+ default: null,
+ },
},
res: convertLog(networkChart.schema),
};
export default define(meta, async (ps) => {
- return await networkChart.getChart(ps.span as any, ps.limit!);
+ return await networkChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
});
diff --git a/src/server/api/endpoints/charts/notes.ts b/src/server/api/endpoints/charts/notes.ts
index c25f46f543..74aa48b36e 100644
--- a/src/server/api/endpoints/charts/notes.ts
+++ b/src/server/api/endpoints/charts/notes.ts
@@ -25,11 +25,16 @@ export const meta = {
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
}
},
+
+ offset: {
+ validator: $.optional.nullable.num,
+ default: null,
+ },
},
res: convertLog(notesChart.schema),
};
export default define(meta, async (ps) => {
- return await notesChart.getChart(ps.span as any, ps.limit!);
+ return await notesChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
});
diff --git a/src/server/api/endpoints/charts/user/drive.ts b/src/server/api/endpoints/charts/user/drive.ts
index 6bfa427403..5aae5bd757 100644
--- a/src/server/api/endpoints/charts/user/drive.ts
+++ b/src/server/api/endpoints/charts/user/drive.ts
@@ -27,6 +27,11 @@ export const meta = {
}
},
+ offset: {
+ validator: $.optional.nullable.num,
+ default: null,
+ },
+
userId: {
validator: $.type(ID),
desc: {
@@ -40,5 +45,5 @@ export const meta = {
};
export default define(meta, async (ps) => {
- return await perUserDriveChart.getChart(ps.span as any, ps.limit!, ps.userId);
+ return await perUserDriveChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
});
diff --git a/src/server/api/endpoints/charts/user/following.ts b/src/server/api/endpoints/charts/user/following.ts
index 0da995e2ec..9d772c39c9 100644
--- a/src/server/api/endpoints/charts/user/following.ts
+++ b/src/server/api/endpoints/charts/user/following.ts
@@ -27,6 +27,11 @@ export const meta = {
}
},
+ offset: {
+ validator: $.optional.nullable.num,
+ default: null,
+ },
+
userId: {
validator: $.type(ID),
desc: {
@@ -40,5 +45,5 @@ export const meta = {
};
export default define(meta, async (ps) => {
- return await perUserFollowingChart.getChart(ps.span as any, ps.limit!, ps.userId);
+ return await perUserFollowingChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
});
diff --git a/src/server/api/endpoints/charts/user/notes.ts b/src/server/api/endpoints/charts/user/notes.ts
index 754ade1228..8de7c0c3e4 100644
--- a/src/server/api/endpoints/charts/user/notes.ts
+++ b/src/server/api/endpoints/charts/user/notes.ts
@@ -27,6 +27,11 @@ export const meta = {
}
},
+ offset: {
+ validator: $.optional.nullable.num,
+ default: null,
+ },
+
userId: {
validator: $.type(ID),
desc: {
@@ -40,5 +45,5 @@ export const meta = {
};
export default define(meta, async (ps) => {
- return await perUserNotesChart.getChart(ps.span as any, ps.limit!, ps.userId);
+ return await perUserNotesChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
});
diff --git a/src/server/api/endpoints/charts/user/reactions.ts b/src/server/api/endpoints/charts/user/reactions.ts
index f3344c6648..4c37305fc3 100644
--- a/src/server/api/endpoints/charts/user/reactions.ts
+++ b/src/server/api/endpoints/charts/user/reactions.ts
@@ -27,6 +27,11 @@ export const meta = {
}
},
+ offset: {
+ validator: $.optional.nullable.num,
+ default: null,
+ },
+
userId: {
validator: $.type(ID),
desc: {
@@ -40,5 +45,5 @@ export const meta = {
};
export default define(meta, async (ps) => {
- return await perUserReactionsChart.getChart(ps.span as any, ps.limit!, ps.userId);
+ return await perUserReactionsChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
});
diff --git a/src/server/api/endpoints/charts/users.ts b/src/server/api/endpoints/charts/users.ts
index 0d7fb7b951..18eec384a6 100644
--- a/src/server/api/endpoints/charts/users.ts
+++ b/src/server/api/endpoints/charts/users.ts
@@ -25,11 +25,16 @@ export const meta = {
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
}
},
+
+ offset: {
+ validator: $.optional.nullable.num,
+ default: null,
+ },
},
res: convertLog(usersChart.schema),
};
export default define(meta, async (ps) => {
- return await usersChart.getChart(ps.span as any, ps.limit!);
+ return await usersChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
});
diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts
index 5bc224450b..dab05c1675 100644
--- a/src/server/api/endpoints/stats.ts
+++ b/src/server/api/endpoints/stats.ts
@@ -60,9 +60,9 @@ export default define(meta, async () => {
Notes.count({ where: { userHost: null }, cache: 3600000 }),
Users.count({ cache: 3600000 }),
Users.count({ where: { host: null }, cache: 3600000 }),
- federationChart.getChart('hour', 1).then(chart => chart.instance.total[0]),
- driveChart.getChart('hour', 1).then(chart => chart.local.totalSize[0]),
- driveChart.getChart('hour', 1).then(chart => chart.remote.totalSize[0]),
+ federationChart.getChart('hour', 1, null).then(chart => chart.instance.total[0]),
+ driveChart.getChart('hour', 1, null).then(chart => chart.local.totalSize[0]),
+ driveChart.getChart('hour', 1, null).then(chart => chart.remote.totalSize[0]),
]);
return {
diff --git a/src/server/api/endpoints/users/search-by-username-and-host.ts b/src/server/api/endpoints/users/search-by-username-and-host.ts
index 81ff19ff6f..bc68f44094 100644
--- a/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -1,7 +1,6 @@
import $ from 'cafy';
import define from '../../define';
import { Users } from '../../../../models';
-import { User } from '../../../../models/entities/user';
export const meta = {
desc: {
@@ -73,14 +72,17 @@ export default define(meta, async (ps, me) => {
q.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' })
}
+ q.orderBy('user.updatedAt', 'DESC');
+
const users = await q.take(ps.limit!).skip(ps.offset).getMany();
return await Users.packMany(users, me, { detail: ps.detail });
- } else {
+ } else if (ps.username) {
let users = await Users.createQueryBuilder('user')
.where('user.host IS NULL')
.andWhere('user.isSuspended = FALSE')
.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' })
+ .orderBy('user.updatedAt', 'DESC')
.take(ps.limit!)
.skip(ps.offset)
.getMany();
@@ -90,6 +92,7 @@ export default define(meta, async (ps, me) => {
.where('user.host IS NOT NULL')
.andWhere('user.isSuspended = FALSE')
.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' })
+ .orderBy('user.updatedAt', 'DESC')
.take(ps.limit! - users.length)
.getMany();
diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts
index dbeb6eb6af..c01f355d8c 100644
--- a/src/server/api/endpoints/users/search.ts
+++ b/src/server/api/endpoints/users/search.ts
@@ -74,6 +74,7 @@ export default define(meta, async (ps, me) => {
.where('user.host IS NULL')
.andWhere('user.isSuspended = FALSE')
.andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
+ .orderBy('user.updatedAt', 'DESC')
.take(ps.limit!)
.skip(ps.offset)
.getMany();
@@ -83,6 +84,7 @@ export default define(meta, async (ps, me) => {
.where('user.host IS NOT NULL')
.andWhere('user.isSuspended = FALSE')
.andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
+ .orderBy('user.updatedAt', 'DESC')
.take(ps.limit! - users.length)
.getMany();
diff --git a/src/server/api/service/discord.ts b/src/server/api/service/discord.ts
index f9f3026aa8..c2bb02453b 100644
--- a/src/server/api/service/discord.ts
+++ b/src/server/api/service/discord.ts
@@ -13,7 +13,7 @@ import { ILocalUser } from '../../../models/entities/user';
import { ensure } from '../../../prelude/ensure';
function getUserToken(ctx: Koa.Context) {
- return ((ctx.headers['cookie'] || '').match(/i=(\w+)/) || [null, null])[1];
+ return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
}
function compareOrigin(ctx: Koa.Context) {
@@ -113,14 +113,10 @@ router.get('/signin/discord', async ctx => {
response_type: 'code'
};
- const expires = 1000 * 60 * 60; // 1h
- ctx.cookies.set('signin_with_discord_session_id', sessid, {
+ ctx.cookies.set('signin_with_discord_sid', sessid, {
path: '/',
- domain: config.host,
secure: config.url.startsWith('https'),
- httpOnly: true,
- expires: new Date(Date.now() + expires),
- maxAge: expires
+ httpOnly: true
});
redis.set(sessid, JSON.stringify(params));
@@ -135,7 +131,7 @@ router.get('/dc/cb', async ctx => {
const oauth2 = await getOAuth2();
if (!userToken) {
- const sessid = ctx.cookies.get('signin_with_discord_session_id');
+ const sessid = ctx.cookies.get('signin_with_discord_sid');
if (!sessid) {
ctx.throw(400, 'invalid session');
@@ -199,7 +195,7 @@ router.get('/dc/cb', async ctx => {
}
const profile = await UserProfiles.createQueryBuilder()
- .where('"integrations"->"discord"->"id" = :id', { id: id })
+ .where(`"integrations"->'discord'->>'id' = :id`, { id: id })
.andWhere('"userHost" IS NULL')
.getOne();
@@ -212,6 +208,7 @@ router.get('/dc/cb', async ctx => {
integrations: {
...profile.integrations,
discord: {
+ id: id,
accessToken: accessToken,
refreshToken: refreshToken,
expiresDate: expiresDate,
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index ec9cce7ad8..e36c43ee38 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -13,7 +13,7 @@ import { ILocalUser } from '../../../models/entities/user';
import { ensure } from '../../../prelude/ensure';
function getUserToken(ctx: Koa.Context) {
- return ((ctx.headers['cookie'] || '').match(/i=(\w+)/) || [null, null])[1];
+ return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
}
function compareOrigin(ctx: Koa.Context) {
@@ -111,14 +111,10 @@ router.get('/signin/github', async ctx => {
state: uuid()
};
- const expires = 1000 * 60 * 60; // 1h
- ctx.cookies.set('signin_with_github_session_id', sessid, {
+ ctx.cookies.set('signin_with_github_sid', sessid, {
path: '/',
- domain: config.host,
secure: config.url.startsWith('https'),
- httpOnly: true,
- expires: new Date(Date.now() + expires),
- maxAge: expires
+ httpOnly: true
});
redis.set(sessid, JSON.stringify(params));
@@ -133,7 +129,7 @@ router.get('/gh/cb', async ctx => {
const oauth2 = await getOath2();
if (!userToken) {
- const sessid = ctx.cookies.get('signin_with_github_session_id');
+ const sessid = ctx.cookies.get('signin_with_github_sid');
if (!sessid) {
ctx.throw(400, 'invalid session');
@@ -192,7 +188,7 @@ router.get('/gh/cb', async ctx => {
}
const link = await UserProfiles.createQueryBuilder()
- .where('"integrations"->"github"->"id" = :id', { id: id })
+ .where(`"integrations"->'github'->>'id' = :id`, { id: id })
.andWhere('"userHost" IS NULL')
.getOne();
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index 881915b58f..000eb57c1b 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -12,7 +12,7 @@ import { ILocalUser } from '../../../models/entities/user';
import { ensure } from '../../../prelude/ensure';
function getUserToken(ctx: Koa.Context) {
- return ((ctx.headers['cookie'] || '').match(/i=(\w+)/) || [null, null])[1];
+ return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
}
function compareOrigin(ctx: Koa.Context) {
@@ -102,14 +102,10 @@ router.get('/signin/twitter', async ctx => {
redis.set(sessid, JSON.stringify(twCtx));
- const expires = 1000 * 60 * 60; // 1h
- ctx.cookies.set('signin_with_twitter_session_id', sessid, {
+ ctx.cookies.set('signin_with_twitter_sid', sessid, {
path: '/',
- domain: config.host,
secure: config.url.startsWith('https'),
- httpOnly: true,
- expires: new Date(Date.now() + expires),
- maxAge: expires
+ httpOnly: true
});
ctx.redirect(twCtx.url);
@@ -121,7 +117,7 @@ router.get('/tw/cb', async ctx => {
const twAuth = await getTwAuth();
if (userToken == null) {
- const sessid = ctx.cookies.get('signin_with_twitter_session_id');
+ const sessid = ctx.cookies.get('signin_with_twitter_sid');
if (sessid == null) {
ctx.throw(400, 'invalid session');
@@ -139,7 +135,7 @@ router.get('/tw/cb', async ctx => {
const result = await twAuth!.done(JSON.parse(twCtx), ctx.query.oauth_verifier);
const link = await UserProfiles.createQueryBuilder()
- .where('"integrations"->"twitter"->"userId" = :id', { id: result.userId })
+ .where(`"integrations"->'twitter'->>'userId' = :id`, { id: result.userId })
.andWhere('"userHost" IS NULL')
.getOne();
diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts
index 8cd4fcac99..22e664baca 100644
--- a/src/server/api/stream/channels/main.ts
+++ b/src/server/api/stream/channels/main.ts
@@ -1,6 +1,6 @@
import autobind from 'autobind-decorator';
import Channel from '../channel';
-import { Mutings, Notes } from '../../../../models';
+import { Notes } from '../../../../models';
export default class extends Channel {
public readonly chName = 'main';
@@ -9,15 +9,14 @@ export default class extends Channel {
@autobind
public async init(params: any) {
- const mute = await Mutings.find({ muterId: this.user!.id });
-
// Subscribe main stream channel
this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
- let { type, body } = data;
+ const { type } = data;
+ let { body } = data;
switch (type) {
case 'notification': {
- if (mute.map(m => m.muteeId).includes(body.userId)) return;
+ if (this.muting.includes(body.userId)) return;
if (body.note && body.note.isHidden) {
body.note = await Notes.pack(body.note.id, this.user, {
detail: true
@@ -26,7 +25,7 @@ export default class extends Channel {
break;
}
case 'mention': {
- if (mute.map(m => m.muteeId).includes(body.userId)) return;
+ if (this.muting.includes(body.userId)) return;
if (body.isHidden) {
body = await Notes.pack(body.id, this.user, {
detail: true
diff --git a/src/services/add-note-to-antenna.ts b/src/services/add-note-to-antenna.ts
index 0055639c0b..88a6613c60 100644
--- a/src/services/add-note-to-antenna.ts
+++ b/src/services/add-note-to-antenna.ts
@@ -38,7 +38,7 @@ export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: U
if (note.renoteId != null) {
_note.renote = await Notes.findOne(note.renoteId).then(ensure);
}
-
+
if (shouldMuteThisNote(_note, mutings.map(x => x.muteeId))) {
return;
}
diff --git a/src/services/chart/core.ts b/src/services/chart/core.ts
index 088c524780..dc09923ae4 100644
--- a/src/services/chart/core.ts
+++ b/src/services/chart/core.ts
@@ -8,9 +8,9 @@ import * as nestedProperty from 'nested-property';
import autobind from 'autobind-decorator';
import Logger from '../logger';
import { Schema } from '../../misc/schema';
-import { EntitySchema, getRepository, Repository, LessThan, MoreThanOrEqual } from 'typeorm';
-import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error';
-import { DateUTC, isTimeSame, isTimeBefore, subtractTimespan } from '../../prelude/time';
+import { EntitySchema, getRepository, Repository, LessThan, Between } from 'typeorm';
+import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '../../prelude/time';
+import { getChartInsertLock } from '../../misc/app-lock';
const logger = new Logger('chart', 'white', process.env.NODE_ENV !== 'test');
@@ -134,6 +134,24 @@ export default abstract class Chart<T extends Record<string, any>> {
}
@autobind
+ private static parseDate(date: Date): [number, number, number, number, number, number, number] {
+ const y = date.getUTCFullYear();
+ const m = date.getUTCMonth();
+ const d = date.getUTCDate();
+ const h = date.getUTCHours();
+ const _m = date.getUTCMinutes();
+ const _s = date.getUTCSeconds();
+ const _ms = date.getUTCMilliseconds();
+
+ return [y, m, d, h, _m, _s, _ms];
+ }
+
+ @autobind
+ private static getCurrentDate() {
+ return Chart.parseDate(new Date());
+ }
+
+ @autobind
public static schemaToEntity(name: string, schema: Schema): EntitySchema {
return new EntitySchema({
name: `__chart__${camelToSnake(name)}`,
@@ -212,18 +230,6 @@ export default abstract class Chart<T extends Record<string, any>> {
}
@autobind
- private getCurrentDate(): [number, number, number, number] {
- const now = new Date();
-
- const y = now.getUTCFullYear();
- const m = now.getUTCMonth();
- const d = now.getUTCDate();
- const h = now.getUTCHours();
-
- return [y, m, d, h];
- }
-
- @autobind
private getLatestLog(span: Span, group: string | null = null): Promise<Log | null> {
return this.repository.findOne({
group: group,
@@ -237,11 +243,11 @@ export default abstract class Chart<T extends Record<string, any>> {
@autobind
private async getCurrentLog(span: Span, group: string | null = null): Promise<Log> {
- const [y, m, d, h] = this.getCurrentDate();
+ const [y, m, d, h] = Chart.getCurrentDate();
const current =
- span == 'day' ? DateUTC([y, m, d]) :
- span == 'hour' ? DateUTC([y, m, d, h]) :
+ span == 'day' ? dateUTC([y, m, d, 0]) :
+ span == 'hour' ? dateUTC([y, m, d, h]) :
null as never;
// 現在(今日または今のHour)のログ
@@ -283,30 +289,35 @@ export default abstract class Chart<T extends Record<string, any>> {
logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): Initial commit created`);
}
+ const date = Chart.dateToTimestamp(current);
+ const lockKey = `${this.name}:${date}:${group}:${span}`;
+
+ const unlock = await getChartInsertLock(lockKey);
try {
+ // ロック内でもう1回チェックする
+ const currentLog = await this.repository.findOne({
+ span: span,
+ date: date,
+ ...(group ? { group: group } : {})
+ });
+
+ // ログがあればそれを返して終了
+ if (currentLog != null) return currentLog;
+
// 新規ログ挿入
log = await this.repository.save({
group: group,
span: span,
- date: Chart.dateToTimestamp(current),
+ date: date,
...Chart.convertObjectToFlattenColumns(data)
});
logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): New commit created`);
- } catch (e) {
- // duplicate key error
- // 並列動作している他のチャートエンジンプロセスと処理が重なる場合がある
- // その場合は再度最も新しいログを持ってくる
- if (isDuplicateKeyValueError(e)) {
- log = await this.getLatestLog(span, group) as Log;
- logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): Commit duplicated`);
- } else {
- logger.error(e);
- throw e;
- }
- }
- return log;
+ return log;
+ } finally {
+ unlock();
+ }
}
@autobind
@@ -373,12 +384,15 @@ export default abstract class Chart<T extends Record<string, any>> {
}
@autobind
- public async getChart(span: Span, range: number, group: string | null = null): Promise<ArrayValue<T>> {
- const [y, m, d, h] = this.getCurrentDate();
+ public async getChart(span: Span, amount: number, begin: Date | null, group: string | null = null): Promise<ArrayValue<T>> {
+ const [y, m, d, h, _m, _s, _ms] = begin ? Chart.parseDate(subtractTime(addTime(begin, 1, span), 1)) : Chart.getCurrentDate();
+ const [y2, m2, d2, h2] = begin ? Chart.parseDate(addTime(begin, 1, span)) : [] as never;
+
+ const lt = dateUTC([y, m, d, h, _m, _s, _ms]);
const gt =
- span == 'day' ? subtractTimespan(DateUTC([y, m, d]), range, 'days') :
- span == 'hour' ? subtractTimespan(DateUTC([y, m, d, h]), range, 'hours') :
+ span === 'day' ? subtractTime(begin ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') :
+ span === 'hour' ? subtractTime(begin ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') :
null as never;
// ログ取得
@@ -386,7 +400,7 @@ export default abstract class Chart<T extends Record<string, any>> {
where: {
group: group,
span: span,
- date: MoreThanOrEqual(Chart.dateToTimestamp(gt))
+ date: Between(Chart.dateToTimestamp(gt), Chart.dateToTimestamp(lt))
},
order: {
date: -1
@@ -432,10 +446,10 @@ export default abstract class Chart<T extends Record<string, any>> {
const chart: T[] = [];
// 整形
- for (let i = (range - 1); i >= 0; i--) {
+ for (let i = (amount - 1); i >= 0; i--) {
const current =
- span == 'day' ? subtractTimespan(DateUTC([y, m, d]), i, 'days') :
- span == 'hour' ? subtractTimespan(DateUTC([y, m, d, h]), i, 'hours') :
+ span === 'day' ? subtractTime(dateUTC([y, m, d, 0]), i, 'day') :
+ span === 'hour' ? subtractTime(dateUTC([y, m, d, h]), i, 'hour') :
null as never;
const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current));
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index 2931de6dc0..cf0951ebad 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -217,7 +217,8 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string,
const upload = s3.upload(params);
- await upload.promise();
+ const result = await upload.promise();
+ if (result) logger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
}
async function deleteOldFile(user: IRemoteUser) {
diff --git a/src/services/drive/s3.ts b/src/services/drive/s3.ts
index 95243d5901..d136bb2694 100644
--- a/src/services/drive/s3.ts
+++ b/src/services/drive/s3.ts
@@ -1,23 +1,23 @@
import * as S3 from 'aws-sdk/clients/s3';
import config from '../../config';
import { Meta } from '../../models/entities/meta';
-import * as httpsProxyAgent from 'https-proxy-agent';
+import { HttpsProxyAgent } from 'https-proxy-agent';
import * as agentkeepalive from 'agentkeepalive';
const httpsAgent = config.proxy
- ? new httpsProxyAgent(config.proxy)
+ ? new HttpsProxyAgent(config.proxy)
: new agentkeepalive.HttpsAgent({
freeSocketTimeout: 30 * 1000
});
export function getS3(meta: Meta) {
const conf = {
- endpoint: meta.objectStorageEndpoint,
+ endpoint: meta.objectStorageEndpoint || undefined,
accessKeyId: meta.objectStorageAccessKey,
secretAccessKey: meta.objectStorageSecretKey,
- region: meta.objectStorageRegion,
+ region: meta.objectStorageRegion || undefined,
sslEnabled: meta.objectStorageUseSSL,
- s3ForcePathStyle: true,
+ s3ForcePathStyle: !!meta.objectStorageEndpoint,
httpOptions: {
}
} as S3.ClientConfiguration;
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 3426083e30..50586e8bc7 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -30,6 +30,7 @@ import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-erro
import { ensure } from '../../prelude/ensure';
import { checkHitAntenna } from '../../misc/check-hit-antenna';
import { addNoteToAntenna } from '../add-note-to-antenna';
+import { countSameRenotes } from '../../misc/count-same-renotes';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -222,7 +223,7 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
.getMany();
const followers = followings.map(f => f.followerId);
-
+
for (const antenna of antennas) {
checkHitAntenna(antenna, note, user, followers).then(hit => {
if (hit) {
@@ -236,7 +237,8 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
saveReply(data.reply, note);
}
- if (data.renote) {
+ // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
+ if (data.renote && (await countSameRenotes(user.id, data.renote.id, note.id) === 0)) {
incRenoteCount(data.renote);
}
diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts
index 29b9c576da..dc8d23134a 100644
--- a/src/services/note/delete.ts
+++ b/src/services/note/delete.ts
@@ -11,6 +11,7 @@ import { Note } from '../../models/entities/note';
import { Notes, Users, Instances } from '../../models';
import { notesChart, perUserNotesChart, instanceChart } from '../chart';
import { deliverToFollowers } from '../../remote/activitypub/deliver-manager';
+import { countSameRenotes } from '../../misc/count-same-renotes';
/**
* 投稿を削除します。
@@ -20,7 +21,8 @@ import { deliverToFollowers } from '../../remote/activitypub/deliver-manager';
export default async function(user: User, note: Note, quiet = false) {
const deletedAt = new Date();
- if (note.renoteId) {
+ // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
+ if (note.renoteId && (await countSameRenotes(user.id, note.renoteId, note.id)) === 0) {
Notes.decrement({ id: note.renoteId }, 'renoteCount', 1);
Notes.decrement({ id: note.renoteId }, 'score', 1);
}