diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2020-03-21 13:36:41 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2020-03-21 13:36:41 +0900 |
| commit | 6fb7721798657cf842556fb63f116316365efa74 (patch) | |
| tree | 6917342553156b6483b899eafc66ef8f040b8352 /src | |
| parent | Update CHANGELOG.md (diff) | |
| parent | 12.22.0 (diff) | |
| download | misskey-6fb7721798657cf842556fb63f116316365efa74.tar.gz misskey-6fb7721798657cf842556fb63f116316365efa74.tar.bz2 misskey-6fb7721798657cf842556fb63f116316365efa74.zip | |
Merge branch 'develop'
Diffstat (limited to 'src')
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); } |