summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-09-22 22:53:41 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-09-22 22:53:41 +0900
commit338793d891d1657f158cd4dc83f998e124bd7e45 (patch)
treed47080ad4fcff61ad5eafdb8eb1e3ca997739115 /src
parentMerge branch 'develop' (diff)
parent12.91.0 (diff)
downloadmisskey-338793d891d1657f158cd4dc83f998e124bd7e45.tar.gz
misskey-338793d891d1657f158cd4dc83f998e124bd7e45.tar.bz2
misskey-338793d891d1657f158cd4dc83f998e124bd7e45.zip
Merge branch 'develop'
Diffstat (limited to 'src')
-rw-r--r--src/client/account.ts22
-rw-r--r--src/client/components/global/avatar.vue26
-rw-r--r--src/client/components/mfm.ts14
-rw-r--r--src/client/components/note-header.vue12
-rw-r--r--src/client/components/page-window.vue2
-rwxr-xr-xsrc/client/components/signin.vue41
-rw-r--r--src/client/components/sparkle.vue180
-rw-r--r--src/client/components/ui/folder.vue3
-rw-r--r--src/client/components/ui/input.vue2
-rw-r--r--src/client/components/ui/menu.vue33
-rw-r--r--src/client/components/ui/textarea.vue2
-rw-r--r--src/client/init.ts9
-rw-r--r--src/client/menu.ts45
-rw-r--r--src/client/os.ts2
-rw-r--r--src/client/pages/antenna-timeline.vue147
-rw-r--r--src/client/pages/emojis.category.vue134
-rw-r--r--src/client/pages/emojis.emoji.vue92
-rw-r--r--src/client/pages/emojis.vue143
-rw-r--r--src/client/pages/favorites.vue3
-rw-r--r--src/client/pages/mfm-cheat-sheet.vue13
-rw-r--r--src/client/pages/note.vue134
-rw-r--r--src/client/pages/notifications.vue1
-rw-r--r--src/client/pages/settings/index.vue3
-rw-r--r--src/client/pages/settings/other.vue2
-rw-r--r--src/client/pages/timeline.vue198
-rw-r--r--src/client/pages/user-list-timeline.vue147
-rw-r--r--src/client/pages/user/index.vue93
-rw-r--r--src/client/router.ts2
-rw-r--r--src/client/scripts/show-suspended-dialog.ts10
-rw-r--r--src/client/style.scss1
-rw-r--r--src/client/themes/_dark.json53
-rw-r--r--src/client/themes/_light.json53
-rw-r--r--src/client/ui/_common_/header.vue110
-rw-r--r--src/client/ui/_common_/sidebar.vue86
-rw-r--r--src/client/ui/default.vue11
-rw-r--r--src/client/ui/universal.vue6
-rw-r--r--src/docs/en-US/advanced/aiscript.md6
-rw-r--r--src/docs/eo-UY/advanced/create-plugin.md4
-rw-r--r--src/docs/eo-UY/features/keyboard-shortcut.md28
-rw-r--r--src/docs/eo-UY/features/mfm.md4
-rw-r--r--src/docs/eo-UY/features/note.md2
-rw-r--r--src/docs/eo-UY/features/timeline.md30
-rw-r--r--src/docs/eo-UY/general/glossary.md34
-rw-r--r--src/docs/eo-UY/general/links.md2
-rw-r--r--src/docs/eo-UY/general/misskey.md2
-rw-r--r--src/docs/eo-UY/general/troubleshooting.md2
-rw-r--r--src/docs/ko-KR/admin/disable-timelines.md10
-rw-r--r--src/docs/ko-KR/admin/faq.md8
-rw-r--r--src/docs/ko-KR/advanced/aiscript.md8
-rw-r--r--src/docs/ko-KR/general/apps.md6
-rw-r--r--src/docs/ko-KR/general/changelog.md4
-rw-r--r--src/docs/ko-KR/general/faq.md38
-rw-r--r--src/docs/ko-KR/general/glossary.md70
-rw-r--r--src/docs/ko-KR/general/links.md16
-rw-r--r--src/docs/ko-KR/general/misskey.md22
-rw-r--r--src/docs/zh-CN/advanced/create-plugin.md10
-rw-r--r--src/docs/zh-CN/advanced/develop-bot.md8
-rw-r--r--src/docs/zh-CN/advanced/reversi-bot.md120
-rw-r--r--src/docs/zh-CN/advanced/stream.md20
-rw-r--r--src/docs/zh-CN/features/timeline.md30
-rw-r--r--src/docs/zh-CN/general/apps.md6
-rw-r--r--src/docs/zh-CN/general/changelog.md4
-rw-r--r--src/docs/zh-CN/general/links.md14
-rw-r--r--src/docs/zh-CN/general/report-issue.md2
-rw-r--r--src/misc/api-permissions.ts1
-rw-r--r--src/misc/check-hit-antenna.ts4
-rw-r--r--src/misc/schema.ts78
-rw-r--r--src/misc/simple-schema.ts15
-rw-r--r--src/models/repositories/antenna.ts6
-rw-r--r--src/models/repositories/app.ts15
-rw-r--r--src/models/repositories/blocking.ts8
-rw-r--r--src/models/repositories/channel.ts6
-rw-r--r--src/models/repositories/clip.ts8
-rw-r--r--src/models/repositories/drive-file.ts14
-rw-r--r--src/models/repositories/drive-folder.ts8
-rw-r--r--src/models/repositories/emoji.ts41
-rw-r--r--src/models/repositories/following.ts10
-rw-r--r--src/models/repositories/gallery-post.ts10
-rw-r--r--src/models/repositories/games/reversi/game.ts150
-rw-r--r--src/models/repositories/games/reversi/matching.ts45
-rw-r--r--src/models/repositories/hashtag.ts6
-rw-r--r--src/models/repositories/messaging-message.ts14
-rw-r--r--src/models/repositories/muting.ts8
-rw-r--r--src/models/repositories/note-favorite.ts2
-rw-r--r--src/models/repositories/note-reaction.ts8
-rw-r--r--src/models/repositories/note.ts41
-rw-r--r--src/models/repositories/notification.ts46
-rw-r--r--src/models/repositories/page.ts8
-rw-r--r--src/models/repositories/user-group.ts6
-rw-r--r--src/models/repositories/user-list.ts6
-rw-r--r--src/models/repositories/user.ts40
-rw-r--r--src/queue/index.ts9
-rw-r--r--src/queue/processors/db/delete-account.ts11
-rw-r--r--src/queue/processors/db/export-blocking.ts2
-rw-r--r--src/queue/processors/db/export-following.ts2
-rw-r--r--src/queue/processors/db/export-mute.ts2
-rw-r--r--src/queue/processors/db/export-notes.ts2
-rw-r--r--src/queue/processors/db/export-user-lists.ts2
-rw-r--r--src/queue/types.ts7
-rw-r--r--src/remote/activitypub/kernel/delete/actor.ts26
-rw-r--r--src/remote/activitypub/kernel/delete/index.ts3
-rw-r--r--src/remote/activitypub/resolver.ts8
-rw-r--r--src/server/api/call.ts7
-rw-r--r--src/server/api/endpoints.ts4
-rw-r--r--src/server/api/endpoints/admin/accounts/delete.ts58
-rw-r--r--src/server/api/endpoints/antennas/notes.ts5
-rw-r--r--src/server/api/endpoints/i/delete-account.ts4
-rw-r--r--src/server/api/endpoints/reset-db.ts2
-rw-r--r--src/server/api/openapi/schemas.ts49
-rw-r--r--src/server/api/private/signin.ts37
-rw-r--r--src/server/api/stream/channels/channel.ts4
-rw-r--r--src/server/api/stream/channels/global-timeline.ts6
-rw-r--r--src/server/api/stream/channels/hashtag.ts4
-rw-r--r--src/server/api/stream/channels/home-timeline.ts6
-rw-r--r--src/server/api/stream/channels/hybrid-timeline.ts9
-rw-r--r--src/server/api/stream/channels/local-timeline.ts9
-rw-r--r--src/server/api/stream/channels/user-list.ts4
-rw-r--r--src/server/api/stream/index.ts12
-rw-r--r--src/server/web/boot.js8
-rw-r--r--src/services/chart/core.ts14
-rw-r--r--src/services/note/read.ts14
-rw-r--r--src/services/push-notification.ts5
122 files changed, 2146 insertions, 1038 deletions
diff --git a/src/client/account.ts b/src/client/account.ts
index e469bae5a2..6e26ac1f7d 100644
--- a/src/client/account.ts
+++ b/src/client/account.ts
@@ -3,6 +3,7 @@ import { reactive } from 'vue';
import { apiUrl } from '@client/config';
import { waiting } from '@client/os';
import { unisonReload, reloadChannel } from '@client/scripts/unison-reload';
+import { showSuspendedDialog } from './scripts/show-suspended-dialog';
// TODO: 他のタブと永続化されたstateを同期
@@ -82,17 +83,20 @@ function fetchAccount(token): Promise<Account> {
i: token
})
})
+ .then(res => res.json())
.then(res => {
- // When failed to authenticate user
- if (res.status !== 200 && res.status < 500) {
- return signout();
+ if (res.error) {
+ if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
+ showSuspendedDialog().then(() => {
+ signout();
+ });
+ } else {
+ signout();
+ }
+ } else {
+ res.token = token;
+ done(res);
}
-
- // Parse response
- res.json().then(i => {
- i.token = token;
- done(i);
- });
})
.catch(fail);
});
diff --git a/src/client/components/global/avatar.vue b/src/client/components/global/avatar.vue
index eea970ec9a..395ed5d8ce 100644
--- a/src/client/components/global/avatar.vue
+++ b/src/client/components/global/avatar.vue
@@ -73,6 +73,22 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
+@keyframes earwiggleleft {
+ from { transform: rotate(37.6deg) skew(30deg); }
+ 25% { transform: rotate(10deg) skew(30deg); }
+ 50% { transform: rotate(20deg) skew(30deg); }
+ 75% { transform: rotate(0deg) skew(30deg); }
+ to { transform: rotate(37.6deg) skew(30deg); }
+}
+
+@keyframes earwiggleright {
+ from { transform: rotate(-37.6deg) skew(-30deg); }
+ 30% { transform: rotate(-10deg) skew(-30deg); }
+ 55% { transform: rotate(-20deg) skew(-30deg); }
+ 75% { transform: rotate(0deg) skew(-30deg); }
+ to { transform: rotate(-37.6deg) skew(-30deg); }
+}
+
.eiwwqkts {
position: relative;
display: inline-block;
@@ -132,6 +148,16 @@ export default defineComponent({
border-radius: 75% 0 75% 75%;
transform: rotate(-37.5deg) skew(-30deg);
}
+
+ &:hover {
+ &:before {
+ animation: earwiggleleft 1s infinite;
+ }
+
+ &:after {
+ animation: earwiggleright 1s infinite;
+ }
+ }
}
}
</style>
diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts
index c248f934df..a228ca4b8d 100644
--- a/src/client/components/mfm.ts
+++ b/src/client/components/mfm.ts
@@ -8,6 +8,7 @@ import { concat } from '@client/../prelude/array';
import MkFormula from '@client/components/formula.vue';
import MkCode from '@client/components/code.vue';
import MkGoogle from '@client/components/google.vue';
+import MkSparkle from '@client/components/sparkle.vue';
import MkA from '@client/components/global/a.vue';
import { host } from '@client/config';
@@ -169,6 +170,19 @@ export default defineComponent({
style = this.$store.state.animatedMfm ? 'animation: mfm-rainbow 1s linear infinite;' : '';
break;
}
+ case 'sparkle': {
+ if (!this.$store.state.animatedMfm) {
+ return genEl(token.children);
+ }
+ let count = token.props.args.count ? parseInt(token.props.args.count) : 10;
+ if (count > 100) {
+ count = 100;
+ }
+ const speed = token.props.args.speed ? parseFloat(token.props.args.speed) : 1;
+ return h(MkSparkle, {
+ count, speed,
+ }, genEl(token.children));
+ }
}
if (style == null) {
return h('span', {}, ['[', token.props.name, ...genEl(token.children), ']']);
diff --git a/src/client/components/note-header.vue b/src/client/components/note-header.vue
index 7758dea3ae..80bfea9b07 100644
--- a/src/client/components/note-header.vue
+++ b/src/client/components/note-header.vue
@@ -3,10 +3,10 @@
<MkA class="name" :to="userPage(note.user)" v-user-preview="note.user.id">
<MkUserName :user="note.user"/>
</MkA>
- <span class="is-bot" v-if="note.user.isBot">bot</span>
- <span class="username"><MkAcct :user="note.user"/></span>
- <span class="admin" v-if="note.user.isAdmin"><i class="fas fa-bookmark"></i></span>
- <span class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><i class="far fa-bookmark"></i></span>
+ <div class="is-bot" v-if="note.user.isBot">bot</div>
+ <div class="username"><MkAcct :user="note.user"/></div>
+ <div class="admin" v-if="note.user.isAdmin"><i class="fas fa-bookmark"></i></div>
+ <div class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><i class="far fa-bookmark"></i></div>
<div class="info">
<span class="mobile" v-if="note.viaMobile"><i class="fas fa-mobile-alt"></i></span>
<MkA class="created-at" :to="notePage(note)">
@@ -55,6 +55,7 @@ export default defineComponent({
white-space: nowrap;
> .name {
+ flex-shrink: 1;
display: block;
margin: 0 .5em 0 0;
padding: 0;
@@ -81,17 +82,20 @@ export default defineComponent({
> .admin,
> .moderator {
+ flex-shrink: 0;
margin-right: 0.5em;
color: var(--badge);
}
> .username {
+ flex-shrink: 9999999;
margin: 0 .5em 0 0;
overflow: hidden;
text-overflow: ellipsis;
}
> .info {
+ flex-shrink: 0;
margin-left: auto;
font-size: 0.9em;
diff --git a/src/client/components/page-window.vue b/src/client/components/page-window.vue
index c83b040dd8..fbc9f0b7fd 100644
--- a/src/client/components/page-window.vue
+++ b/src/client/components/page-window.vue
@@ -8,7 +8,7 @@
@closed="$emit('closed')"
>
<template #header>
- <XHeader :info="pageInfo" :back-button="history.length > 0" @back="back()" :close-button="true" @close="close()"/>
+ <XHeader :info="pageInfo" :back-button="history.length > 0" @back="back()" :close-button="true" @close="close()" :title-only="true"/>
</template>
<div class="yrolvcoq _flat_">
<component :is="component" v-bind="props" :ref="changePage"/>
diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue
index c051288d0a..69f527b7d6 100755
--- a/src/client/components/signin.vue
+++ b/src/client/components/signin.vue
@@ -54,6 +54,7 @@ import { apiUrl, host } from '@client/config';
import { byteify, hexify } from '@client/scripts/2fa';
import * as os from '@client/os';
import { login } from '@client/account';
+import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
export default defineComponent({
components: {
@@ -169,15 +170,7 @@ export default defineComponent({
this.signing = false;
this.challengeData = res;
return this.queryKey();
- }).catch(() => {
- os.dialog({
- type: 'error',
- text: this.$ts.signinFailed
- });
- this.challengeData = null;
- this.totpLogin = false;
- this.signing = false;
- });
+ }).catch(this.loginFailed);
} else {
this.totpLogin = true;
this.signing = false;
@@ -190,14 +183,36 @@ export default defineComponent({
}).then(res => {
this.$emit('login', res);
this.onLogin(res);
- }).catch(() => {
+ }).catch(this.loginFailed);
+ }
+ },
+
+ loginFailed(err) {
+ switch (err.id) {
+ case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
os.dialog({
type: 'error',
- text: this.$ts.loginFailed
+ title: this.$ts.loginFailed,
+ text: this.$ts.noSuchUser
});
- this.signing = false;
- });
+ break;
+ }
+ case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
+ showSuspendedDialog();
+ break;
+ }
+ default: {
+ os.dialog({
+ type: 'error',
+ title: this.$ts.loginFailed,
+ text: JSON.stringify(err)
+ });
+ }
}
+
+ this.challengeData = null;
+ this.totpLogin = false;
+ this.signing = false;
},
resetPassword() {
diff --git a/src/client/components/sparkle.vue b/src/client/components/sparkle.vue
new file mode 100644
index 0000000000..942412b445
--- /dev/null
+++ b/src/client/components/sparkle.vue
@@ -0,0 +1,180 @@
+<template>
+<span class="mk-sparkle">
+ <span ref="content">
+ <slot></slot>
+ </span>
+ <canvas ref="canvas"></canvas>
+</span>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@client/os';
+
+const sprite = new Image();
+sprite.src = "/static-assets/client/sparkle-spritesheet.png";
+
+
+export default defineComponent({
+ props: {
+ count: {
+ type: Number,
+ required: true,
+ },
+ speed: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ sprites: [0,6,13,20],
+ particles: [],
+ anim: null,
+ ctx: null,
+ };
+ },
+ methods: {
+ createSparkles(w, h, count) {
+ var holder = [];
+
+ for (var i = 0; i < count; i++) {
+
+ const color = '#' + ('000000' + Math.floor(Math.random() * 16777215).toString(16)).slice(-6);
+
+ holder[i] = {
+ position: {
+ x: Math.floor(Math.random() * w),
+ y: Math.floor(Math.random() * h)
+ },
+ style: this.sprites[ Math.floor(Math.random() * 4) ],
+ delta: {
+ x: Math.floor(Math.random() * 1000) - 500,
+ y: Math.floor(Math.random() * 1000) - 500
+ },
+ color: color,
+ opacity: Math.random(),
+ };
+
+ }
+
+ return holder;
+ },
+ draw(time) {
+ this.ctx.clearRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
+ this.ctx.beginPath();
+
+ const particleSize = Math.floor(this.fontSize / 2);
+ this.particles.forEach((particle) => {
+ var modulus = Math.floor(Math.random()*7);
+
+ if (Math.floor(time) % modulus === 0) {
+ particle.style = this.sprites[ Math.floor(Math.random()*4) ];
+ }
+
+ this.ctx.save();
+ this.ctx.globalAlpha = particle.opacity;
+ this.ctx.drawImage(sprite, particle.style, 0, 7, 7, particle.position.x, particle.position.y, particleSize, particleSize);
+
+ this.ctx.globalCompositeOperation = "source-atop";
+ this.ctx.globalAlpha = 0.5;
+ this.ctx.fillStyle = particle.color;
+ this.ctx.fillRect(particle.position.x, particle.position.y, particleSize, particleSize);
+
+ this.ctx.restore();
+ });
+ this.ctx.stroke();
+ },
+ tick() {
+ this.anim = window.requestAnimationFrame((time) => {
+ if (!this.$refs.canvas) {
+ return;
+ }
+ this.particles.forEach((particle) => {
+ if (!particle) {
+ return;
+ }
+ var randX = Math.random() > Math.random() * 2;
+ var randY = Math.random() > Math.random() * 3;
+
+ if (randX) {
+ particle.position.x += (particle.delta.x * this.speed) / 1500;
+ }
+
+ if (!randY) {
+ particle.position.y -= (particle.delta.y * this.speed) / 800;
+ }
+
+ if( particle.position.x > this.$refs.canvas.width ) {
+ particle.position.x = -7;
+ } else if (particle.position.x < -7) {
+ particle.position.x = this.$refs.canvas.width;
+ }
+
+ if (particle.position.y > this.$refs.canvas.height) {
+ particle.position.y = -7;
+ particle.position.x = Math.floor(Math.random() * this.$refs.canvas.width);
+ } else if (particle.position.y < -7) {
+ particle.position.y = this.$refs.canvas.height;
+ particle.position.x = Math.floor(Math.random() * this.$refs.canvas.width);
+ }
+
+ particle.opacity -= 0.005;
+
+ if (particle.opacity <= 0) {
+ particle.opacity = 1;
+ }
+ });
+
+ this.draw(time);
+
+ this.tick();
+ });
+ },
+ resize() {
+ if (this.$refs.content) {
+ const contentRect = this.$refs.content.getBoundingClientRect();
+ this.fontSize = parseFloat(getComputedStyle(this.$refs.content).fontSize);
+ const padding = this.fontSize * 0.2;
+
+ this.$refs.canvas.width = parseInt(contentRect.width + padding);
+ this.$refs.canvas.height = parseInt(contentRect.height + padding);
+
+ this.particles = this.createSparkles(this.$refs.canvas.width, this.$refs.canvas.height, this.count);
+ }
+ },
+ },
+ mounted() {
+ this.ctx = this.$refs.canvas.getContext('2d');
+
+ new ResizeObserver(this.resize).observe(this.$refs.content);
+
+ this.resize();
+ this.tick();
+ },
+ updated() {
+ this.resize();
+ },
+ destroyed() {
+ window.cancelAnimationFrame(this.anim);
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-sparkle {
+ position: relative;
+ display: inline-block;
+
+ > span {
+ display: inline-block;
+ }
+
+ > canvas {
+ position: absolute;
+ top: -0.1em;
+ left: -0.1em;
+ pointer-events: none;
+ }
+}
+</style>
diff --git a/src/client/components/ui/folder.vue b/src/client/components/ui/folder.vue
index 1f3593a74a..eecf1d8be1 100644
--- a/src/client/components/ui/folder.vue
+++ b/src/client/components/ui/folder.vue
@@ -99,7 +99,8 @@ export default defineComponent({
z-index: 10;
position: sticky;
top: var(--stickyTop, 0px);
- background: var(--panel);
+ padding: var(--x-padding);
+ background: var(--x-header, var(--panel));
/* TODO panelの半透明バージョンをプログラマティックに作りたい
background: var(--X17);
-webkit-backdrop-filter: var(--blur, blur(8px));
diff --git a/src/client/components/ui/input.vue b/src/client/components/ui/input.vue
index 05ce5d3e15..a916a0b035 100644
--- a/src/client/components/ui/input.vue
+++ b/src/client/components/ui/input.vue
@@ -245,7 +245,7 @@ export default defineComponent({
font-size: 1em;
color: var(--fg);
background: var(--panel);
- border: solid 1px var(--inputBorder);
+ border: solid 0.5px var(--inputBorder);
border-radius: 6px;
outline: none;
box-shadow: none;
diff --git a/src/client/components/ui/menu.vue b/src/client/components/ui/menu.vue
index d652d9b84f..26b4b04b11 100644
--- a/src/client/components/ui/menu.vue
+++ b/src/client/components/ui/menu.vue
@@ -41,7 +41,7 @@
</template>
<script lang="ts">
-import { defineComponent, ref } from 'vue';
+import { defineComponent, ref, unref } from 'vue';
import { focusPrev, focusNext } from '@client/scripts/focus';
import contains from '@client/scripts/contains';
@@ -79,21 +79,26 @@ export default defineComponent({
};
},
},
- created() {
- const items = ref(this.items.filter(item => item !== undefined));
+ watch: {
+ items: {
+ handler() {
+ const items = ref(unref(this.items).filter(item => item !== undefined));
- for (let i = 0; i < items.value.length; i++) {
- const item = items.value[i];
-
- if (item && item.then) { // if item is Promise
- items.value[i] = { type: 'pending' };
- item.then(actualItem => {
- items.value[i] = actualItem;
- });
- }
- }
+ for (let i = 0; i < items.value.length; i++) {
+ const item = items.value[i];
+
+ if (item && item.then) { // if item is Promise
+ items.value[i] = { type: 'pending' };
+ item.then(actualItem => {
+ items.value[i] = actualItem;
+ });
+ }
+ }
- this._items = items;
+ this._items = items;
+ },
+ immediate: true
+ }
},
mounted() {
if (this.viaKeyboard) {
diff --git a/src/client/components/ui/textarea.vue b/src/client/components/ui/textarea.vue
index 53a141f011..08ac3182a9 100644
--- a/src/client/components/ui/textarea.vue
+++ b/src/client/components/ui/textarea.vue
@@ -212,7 +212,7 @@ export default defineComponent({
font-size: 1em;
color: var(--fg);
background: var(--panel);
- border: solid 1px var(--inputBorder);
+ border: solid 0.5px var(--inputBorder);
border-radius: 6px;
outline: none;
box-shadow: none;
diff --git a/src/client/init.ts b/src/client/init.ts
index 4d2170e03f..c15374e49b 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -15,7 +15,7 @@ if (localStorage.getItem('accounts') != null) {
import * as Sentry from '@sentry/browser';
import { Integrations } from '@sentry/tracing';
-import { computed, createApp, watch, markRaw } from 'vue';
+import { computed, createApp, watch, markRaw, version as vueVersion } from 'vue';
import compareVersions from 'compare-versions';
import widgets from '@client/widgets';
@@ -47,6 +47,8 @@ window.onunhandledrejection = null;
if (_DEV_) {
console.warn('Development mode!!!');
+ console.info(`vue ${vueVersion}`);
+
(window as any).$i = $i;
(window as any).$store = defaultStore;
@@ -215,7 +217,10 @@ if (lastVersion !== version) {
try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
- popup(import('@client/components/updated.vue'), {}, {}, 'closed');
+ // ログインしてる場合だけ
+ if ($i) {
+ popup(import('@client/components/updated.vue'), {}, {}, 'closed');
+ }
}
} catch (e) {
}
diff --git a/src/client/menu.ts b/src/client/menu.ts
index 8e65496cf3..0a9e2b5475 100644
--- a/src/client/menu.ts
+++ b/src/client/menu.ts
@@ -1,9 +1,10 @@
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import { search } from '@client/scripts/search';
import * as os from '@client/os';
import { i18n } from '@client/i18n';
import { $i } from './account';
import { unisonReload } from '@client/scripts/unison-reload';
+import { router } from './router';
export const menuDef = {
notifications: {
@@ -58,7 +59,26 @@ export const menuDef = {
title: 'lists',
icon: 'fas fa-list-ul',
show: computed(() => $i != null),
- to: '/my/lists',
+ active: computed(() => router.currentRoute.value.path.startsWith('/timeline/list/') || router.currentRoute.value.path === '/my/lists' || router.currentRoute.value.path.startsWith('/my/lists/')),
+ action: (ev) => {
+ const items = ref([{
+ type: 'pending'
+ }]);
+ os.api('users/lists/list').then(lists => {
+ const _items = [...lists.map(list => ({
+ type: 'link',
+ text: list.name,
+ to: `/timeline/list/${list.id}`
+ })), null, {
+ type: 'link',
+ to: '/my/lists',
+ text: i18n.locale.manageLists,
+ icon: 'fas fa-cog',
+ }];
+ items.value = _items;
+ });
+ os.popupMenu(items, ev.currentTarget || ev.target);
+ },
},
groups: {
title: 'groups',
@@ -70,7 +90,26 @@ export const menuDef = {
title: 'antennas',
icon: 'fas fa-satellite',
show: computed(() => $i != null),
- to: '/my/antennas',
+ active: computed(() => router.currentRoute.value.path.startsWith('/timeline/antenna/') || router.currentRoute.value.path === '/my/antennas' || router.currentRoute.value.path.startsWith('/my/antennas/')),
+ action: (ev) => {
+ const items = ref([{
+ type: 'pending'
+ }]);
+ os.api('antennas/list').then(antennas => {
+ const _items = [...antennas.map(antenna => ({
+ type: 'link',
+ text: antenna.name,
+ to: `/timeline/antenna/${antenna.id}`
+ })), null, {
+ type: 'link',
+ to: '/my/antennas',
+ text: i18n.locale.manageAntennas,
+ icon: 'fas fa-cog',
+ }];
+ items.value = _items;
+ });
+ os.popupMenu(items, ev.currentTarget || ev.target);
+ },
},
mentions: {
title: 'mentions',
diff --git a/src/client/os.ts b/src/client/os.ts
index 8125332798..7ae774dd92 100644
--- a/src/client/os.ts
+++ b/src/client/os.ts
@@ -372,7 +372,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
});
}
-export function popupMenu(items: any[], src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) {
+export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) {
return new Promise((resolve, reject) => {
let dispose;
popup(import('@client/components/ui/popup-menu.vue'), {
diff --git a/src/client/pages/antenna-timeline.vue b/src/client/pages/antenna-timeline.vue
new file mode 100644
index 0000000000..425bec6987
--- /dev/null
+++ b/src/client/pages/antenna-timeline.vue
@@ -0,0 +1,147 @@
+<template>
+<div class="tqmomfks" v-hotkey.global="keymap" v-size="{ min: [800] }">
+ <div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
+ <div class="tl _block">
+ <XTimeline ref="tl" class="tl"
+ :key="antennaId"
+ src="antenna"
+ :antenna="antennaId"
+ :sound="true"
+ @before="before()"
+ @after="after()"
+ @queue="queueUpdated"
+ />
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent, computed } from 'vue';
+import Progress from '@client/scripts/loading';
+import XTimeline from '@client/components/timeline.vue';
+import { scroll } from '@client/scripts/scroll';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+ components: {
+ XTimeline,
+ },
+
+ props: {
+ antennaId: {
+ type: String,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ antenna: null,
+ queue: 0,
+ [symbols.PAGE_INFO]: computed(() => this.antenna ? {
+ title: this.antenna.name,
+ icon: 'fas fa-satellite',
+ bg: 'var(--bg)',
+ actions: [{
+ icon: 'fas fa-calendar-alt',
+ text: this.$ts.jumpToSpecifiedDate,
+ handler: this.timetravel
+ }, {
+ icon: 'fas fa-cog',
+ text: this.$ts.settings,
+ handler: this.settings
+ }],
+ } : null),
+ };
+ },
+
+ computed: {
+ keymap(): any {
+ return {
+ 't': this.focus
+ };
+ },
+ },
+
+ watch: {
+ antennaId: {
+ async handler() {
+ this.antenna = await os.api('antennas/show', {
+ antennaId: this.antennaId
+ });
+ },
+ immediate: true
+ }
+ },
+
+ methods: {
+ before() {
+ Progress.start();
+ },
+
+ after() {
+ Progress.done();
+ },
+
+ queueUpdated(q) {
+ this.queue = q;
+ },
+
+ top() {
+ scroll(this.$el, 0);
+ },
+
+ async timetravel() {
+ const { canceled, result: date } = await os.dialog({
+ title: this.$ts.date,
+ input: {
+ type: 'date'
+ }
+ });
+ if (canceled) return;
+
+ this.$refs.tl.timetravel(new Date(date));
+ },
+
+ settings() {
+ this.$router.push(`/my/antennas/${this.antennaId}`);
+ },
+
+ focus() {
+ (this.$refs.tl as any).focus();
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.tqmomfks {
+ padding: var(--margin);
+
+ > .new {
+ position: sticky;
+ top: calc(var(--stickyTop, 0px) + 16px);
+ z-index: 1000;
+ width: 100%;
+
+ > button {
+ display: block;
+ margin: var(--margin) auto 0 auto;
+ padding: 8px 16px;
+ border-radius: 32px;
+ }
+ }
+
+ > .tl {
+ background: var(--bg);
+ border-radius: var(--radius);
+ overflow: clip;
+ }
+
+ &.min-width_800px {
+ max-width: 800px;
+ margin: 0 auto;
+ }
+}
+</style>
diff --git a/src/client/pages/emojis.category.vue b/src/client/pages/emojis.category.vue
new file mode 100644
index 0000000000..091c3f20a9
--- /dev/null
+++ b/src/client/pages/emojis.category.vue
@@ -0,0 +1,134 @@
+<template>
+<div class="driuhtrh">
+ <div class="query">
+ <MkInput v-model="q" class="_inputNoTopMargin _inputNoBottomMargin" :placeholder="$ts.search">
+ <template #prefix><i class="fas fa-search"></i></template>
+ </MkInput>
+
+ <div class="tags">
+ <span class="tag _button" v-for="tag in tags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span>
+ </div>
+ </div>
+
+ <MkFolder class="emojis" v-if="searchEmojis">
+ <template #header>{{ $ts.searchResult }}</template>
+ <div class="zuvgdzyt">
+ <XEmoji v-for="emoji in searchEmojis" :key="emoji.name" class="emoji" :emoji="emoji"/>
+ </div>
+ </MkFolder>
+
+ <MkFolder class="emojis" v-for="category in customEmojiCategories" :key="category">
+ <template #header>{{ category || $ts.other }}</template>
+ <div class="zuvgdzyt">
+ <XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/>
+ </div>
+ </MkFolder>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed } from 'vue';
+import MkButton from '@client/components/ui/button.vue';
+import MkInput from '@client/components/ui/input.vue';
+import MkSelect from '@client/components/ui/select.vue';
+import MkFolder from '@client/components/ui/folder.vue';
+import MkTab from '@client/components/tab.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { emojiCategories, emojiTags } from '@client/instance';
+import XEmoji from './emojis.emoji.vue';
+
+export default defineComponent({
+ components: {
+ MkButton,
+ MkInput,
+ MkSelect,
+ MkFolder,
+ MkTab,
+ XEmoji,
+ },
+
+ data() {
+ return {
+ q: '',
+ customEmojiCategories: emojiCategories,
+ customEmojis: this.$instance.emojis,
+ tags: emojiTags,
+ selectedTags: new Set(),
+ searchEmojis: null,
+ }
+ },
+
+ watch: {
+ q() { this.search(); },
+ selectedTags: {
+ handler() {
+ this.search();
+ },
+ deep: true
+ },
+ },
+
+ methods: {
+ search() {
+ if ((this.q === '' || this.q == null) && this.selectedTags.size === 0) {
+ this.searchEmojis = null;
+ return;
+ }
+
+ if (this.selectedTags.size === 0) {
+ this.searchEmojis = this.customEmojis.filter(e => e.name.includes(this.q) || e.aliases.includes(this.q));
+ } else {
+ this.searchEmojis = this.customEmojis.filter(e => (e.name.includes(this.q) || e.aliases.includes(this.q)) && [...this.selectedTags].every(t => e.aliases.includes(t)));
+ }
+ },
+
+ toggleTag(tag) {
+ if (this.selectedTags.has(tag)) {
+ this.selectedTags.delete(tag);
+ } else {
+ this.selectedTags.add(tag);
+ }
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.driuhtrh {
+ background: var(--bg);
+
+ > .query {
+ background: var(--bg);
+ padding: 16px;
+
+ > .tags {
+ > .tag {
+ display: inline-block;
+ margin: 8px 8px 0 0;
+ padding: 4px 8px;
+ font-size: 0.9em;
+ background: var(--accentedBg);
+ border-radius: 5px;
+
+ &.active {
+ background: var(--accent);
+ color: var(--fgOnAccent);
+ }
+ }
+ }
+ }
+
+ > .emojis {
+ --x-header: var(--bg);
+ --x-padding: 0 16px;
+
+ .zuvgdzyt {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
+ grid-gap: 12px;
+ margin: 0 var(--margin) var(--margin) var(--margin);
+ }
+ }
+}
+</style>
diff --git a/src/client/pages/emojis.emoji.vue b/src/client/pages/emojis.emoji.vue
new file mode 100644
index 0000000000..3c9bb4debe
--- /dev/null
+++ b/src/client/pages/emojis.emoji.vue
@@ -0,0 +1,92 @@
+<template>
+<button class="zuvgdzyu _button" @click="menu">
+ <img :src="emoji.url" class="img" :alt="emoji.name"/>
+ <div class="body">
+ <div class="name _monospace">{{ emoji.name }}</div>
+ <div class="info">{{ emoji.aliases.join(' ') }}</div>
+ </div>
+</button>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@client/os';
+import copyToClipboard from '@client/scripts/copy-to-clipboard';
+import VanillaTilt from 'vanilla-tilt';
+
+export default defineComponent({
+ props: {
+ emoji: {
+ type: Object,
+ required: true,
+ }
+ },
+
+ mounted() {
+ VanillaTilt.init(this.$el, {
+ reverse: true,
+ gyroscope: false,
+ scale: 1.1,
+ speed: 500,
+ });
+ },
+
+ methods: {
+ menu(ev) {
+ os.popupMenu([{
+ type: 'label',
+ text: ':' + this.emoji.name + ':',
+ }, {
+ text: this.$ts.copy,
+ icon: 'fas fa-copy',
+ action: () => {
+ copyToClipboard(`:${this.emoji.name}:`);
+ os.success();
+ }
+ }], ev.currentTarget || ev.target);
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.zuvgdzyu {
+ display: flex;
+ align-items: center;
+ padding: 12px;
+ text-align: left;
+ background: var(--panel);
+ border-radius: 8px;
+ transform-style: preserve-3d;
+ transform: perspective(1000px);
+
+ &:hover {
+ border-color: var(--accent);
+ }
+
+ > .img {
+ width: 42px;
+ height: 42px;
+ transform: translateZ(20px);
+ }
+
+ > .body {
+ padding: 0 0 0 8px;
+ white-space: nowrap;
+ overflow: hidden;
+ transform: translateZ(10px);
+
+ > .name {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+
+ > .info {
+ opacity: 0.5;
+ font-size: 0.9em;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
+}
+</style>
diff --git a/src/client/pages/emojis.vue b/src/client/pages/emojis.vue
index 391aff8297..8918de2338 100644
--- a/src/client/pages/emojis.vue
+++ b/src/client/pages/emojis.vue
@@ -1,151 +1,36 @@
<template>
-<div class="driuhtrh">
- <div class="query">
- <MkInput v-model="q" class="_inputNoTopMargin _inputNoBottomMargin" :placeholder="$ts.search">
- <template #prefix><i class="fas fa-search"></i></template>
- </MkInput>
- </div>
-
- <div class="emojis">
- <MkFolder v-if="searchEmojis">
- <template #header>{{ $ts.searchResult }}</template>
- <div class="zuvgdzyt">
- <button v-for="emoji in searchEmojis" :key="emoji.name" class="emoji _button" @click="menu(emoji, $event)">
- <img :src="emoji.url" class="img" :alt="emoji.name"/>
- <div class="body">
- <div class="name _monospace">{{ emoji.name }}</div>
- <div class="info">{{ emoji.aliases.join(' ') }}</div>
- </div>
- </button>
- </div>
- </MkFolder>
- <MkFolder v-for="category in customEmojiCategories" :key="category">
- <template #header>{{ category || $ts.other }}</template>
- <div class="zuvgdzyt">
- <button v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji _button" @click="menu(emoji, $event)">
- <img :src="emoji.url" class="img" :alt="emoji.name"/>
- <div class="body">
- <div class="name _monospace">{{ emoji.name }}</div>
- <div class="info">{{ emoji.aliases.join(' ') }}</div>
- </div>
- </button>
- </div>
- </MkFolder>
- </div>
+<div :class="$style.root">
+ <XCategory v-if="tab === 'category'"/>
</div>
</template>
<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkSelect from '@client/components/ui/select.vue';
-import MkFolder from '@client/components/ui/folder.vue';
+import { defineComponent, computed } from 'vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
-import { emojiCategories } from '@client/instance';
-import copyToClipboard from '@client/scripts/copy-to-clipboard';
+import XCategory from './emojis.category.vue';
export default defineComponent({
components: {
- MkButton,
- MkInput,
- MkSelect,
- MkFolder,
+ XCategory,
},
data() {
return {
- [symbols.PAGE_INFO]: {
+ [symbols.PAGE_INFO]: computed(() => ({
title: this.$ts.customEmojis,
- icon: 'fas fa-laugh'
- },
- q: '',
- customEmojiCategories: emojiCategories,
- customEmojis: this.$instance.emojis,
- searchEmojis: null,
- }
- },
-
- watch: {
- q() {
- if (this.q === '' || this.q == null) {
- this.searchEmojis = null;
- return;
- }
-
- this.searchEmojis = this.customEmojis.filter(e => e.name.includes(this.q) || e.aliases.includes(this.q));
+ icon: 'fas fa-laugh',
+ bg: 'var(--bg)',
+ })),
+ tab: 'category',
}
},
-
- methods: {
- menu(emoji, ev) {
- os.popupMenu([{
- type: 'label',
- text: ':' + emoji.name + ':',
- }, {
- text: this.$ts.copy,
- icon: 'fas fa-copy',
- action: () => {
- copyToClipboard(`:${emoji.name}:`);
- os.success();
- }
- }], ev.currentTarget || ev.target);
- }
- }
});
</script>
-<style lang="scss" scoped>
-.driuhtrh {
- > .query {
- background: var(--bg);
- padding: 16px;
- }
-
- > .emojis {
- .zuvgdzyt {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
- grid-gap: 12px;
- margin: 0 var(--margin) var(--margin) var(--margin);
-
- > .emoji {
- display: flex;
- align-items: center;
- padding: 12px;
- text-align: left;
- border: solid 1px var(--divider);
- border-radius: 8px;
-
- &:hover {
- border-color: var(--accent);
- }
-
- > .img {
- width: 42px;
- height: 42px;
- }
-
- > .body {
- padding: 0 0 0 8px;
- white-space: nowrap;
- overflow: hidden;
-
- > .name {
- text-overflow: ellipsis;
- overflow: hidden;
- }
-
- > .info {
- opacity: 0.5;
- font-size: 0.9em;
- text-overflow: ellipsis;
- overflow: hidden;
- }
- }
- }
- }
- }
+<style lang="scss" module>
+.root {
+ max-width: 1000px;
+ margin: 0 auto;
}
</style>
diff --git a/src/client/pages/favorites.vue b/src/client/pages/favorites.vue
index a2d61b98d9..f13723c2d1 100644
--- a/src/client/pages/favorites.vue
+++ b/src/client/pages/favorites.vue
@@ -22,7 +22,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.favorites,
- icon: 'fas fa-star'
+ icon: 'fas fa-star',
+ bg: 'var(--bg)',
},
pagination: {
endpoint: 'i/favorites',
diff --git a/src/client/pages/mfm-cheat-sheet.vue b/src/client/pages/mfm-cheat-sheet.vue
index 95ddc1cbd1..314b5e2a5f 100644
--- a/src/client/pages/mfm-cheat-sheet.vue
+++ b/src/client/pages/mfm-cheat-sheet.vue
@@ -271,6 +271,16 @@
</div>
</div>
</div>
+ <div class="section _block">
+ <div class="title">{{ $ts._mfm.sparkle }}</div>
+ <div class="content">
+ <p>{{ $ts._mfm.sparkleDescription }}</p>
+ <div class="preview">
+ <Mfm :text="preview_sparkle"/>
+ <MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea>
+ </div>
+ </div>
+ </div>
</div>
</template>
@@ -294,7 +304,7 @@ export default defineComponent({
preview_hashtag: '#test',
preview_url: `https://example.com`,
preview_link: `[${this.$ts._mfm.dummy}](https://example.com)`,
- preview_emoji: `:${this.$instance.emojis[0].name}:`,
+ preview_emoji: this.$instance.emojis.length ? `:${this.$instance.emojis[0].name}:` : `:emojiname:`,
preview_bold: `**${this.$ts._mfm.dummy}**`,
preview_small: `<small>${this.$ts._mfm.dummy}</small>`,
preview_center: `<center>${this.$ts._mfm.dummy}</center>`,
@@ -317,6 +327,7 @@ export default defineComponent({
preview_x4: `$[x4 🍮]`,
preview_blur: `$[blur ${this.$ts._mfm.dummy}]`,
preview_rainbow: `$[rainbow 🍮]`,
+ preview_sparkle: `$[sparkle 🍮]`,
}
},
});
diff --git a/src/client/pages/note.vue b/src/client/pages/note.vue
index 7725ca14b4..fe85d7364e 100644
--- a/src/client/pages/note.vue
+++ b/src/client/pages/note.vue
@@ -1,37 +1,39 @@
<template>
-<div class="fcuexfpr _root">
- <transition name="fade" mode="out-in">
- <div v-if="note" class="note">
- <div class="_gap" v-if="showNext">
- <XNotes class="_content" :pagination="next" :no-gap="true"/>
- </div>
-
- <div class="main _gap">
- <MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><i class="fas fa-chevron-up"></i></MkButton>
- <div class="note _gap">
- <MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_isolated"/>
- <XNoteDetailed v-model:note="note" :key="note.id" class="_isolated note"/>
+<div class="fcuexfpr">
+ <div class="_root">
+ <transition name="fade" mode="out-in">
+ <div v-if="note" class="note">
+ <div class="_gap" v-if="showNext">
+ <XNotes class="_content" :pagination="next" :no-gap="true"/>
</div>
- <div class="_content clips _gap" v-if="clips && clips.length > 0">
- <div class="title">{{ $ts.clip }}</div>
- <MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
- <b>{{ item.name }}</b>
- <div v-if="item.description" class="description">{{ item.description }}</div>
- <div class="user">
- <MkAvatar :user="item.user" class="avatar" :show-indicator="true"/> <MkUserName :user="item.user" :nowrap="false"/>
- </div>
- </MkA>
+
+ <div class="main _gap">
+ <MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><i class="fas fa-chevron-up"></i></MkButton>
+ <div class="note _gap">
+ <MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_isolated"/>
+ <XNoteDetailed v-model:note="note" :key="note.id" class="_isolated note"/>
+ </div>
+ <div class="_content clips _gap" v-if="clips && clips.length > 0">
+ <div class="title">{{ $ts.clip }}</div>
+ <MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
+ <b>{{ item.name }}</b>
+ <div v-if="item.description" class="description">{{ item.description }}</div>
+ <div class="user">
+ <MkAvatar :user="item.user" class="avatar" :show-indicator="true"/> <MkUserName :user="item.user" :nowrap="false"/>
+ </div>
+ </MkA>
+ </div>
+ <MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><i class="fas fa-chevron-down"></i></MkButton>
</div>
- <MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><i class="fas fa-chevron-down"></i></MkButton>
- </div>
- <div class="_gap" v-if="showPrev">
- <XNotes class="_content" :pagination="prev" :no-gap="true"/>
+ <div class="_gap" v-if="showPrev">
+ <XNotes class="_content" :pagination="prev" :no-gap="true"/>
+ </div>
</div>
- </div>
- <MkError v-else-if="error" @retry="fetch()"/>
- <MkLoading v-else/>
- </transition>
+ <MkError v-else-if="error" @retry="fetch()"/>
+ <MkLoading v-else/>
+ </transition>
+ </div>
</div>
</template>
@@ -63,12 +65,14 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: computed(() => this.note ? {
title: this.$ts.note,
+ subtitle: new Date(this.note.createdAt).toLocaleString(),
avatar: this.note.user,
path: `/notes/${this.note.id}`,
share: {
title: this.$t('noteOf', { user: this.note.user.name }),
text: this.note.text,
},
+ bg: 'var(--bg)',
} : null),
note: null,
clips: null,
@@ -149,52 +153,54 @@ export default defineComponent({
.fcuexfpr {
background: var(--bg);
- > .note {
- > .main {
- > .load {
- min-width: 0;
- margin: 0 auto;
- border-radius: 999px;
+ > ._root {
+ > .note {
+ > .main {
+ > .load {
+ min-width: 0;
+ margin: 0 auto;
+ border-radius: 999px;
- &.next {
- margin-bottom: var(--margin);
- }
+ &.next {
+ margin-bottom: var(--margin);
+ }
- &.prev {
- margin-top: var(--margin);
+ &.prev {
+ margin-top: var(--margin);
+ }
}
- }
- > .note {
> .note {
- border-radius: var(--radius);
- background: var(--panel);
+ > .note {
+ border-radius: var(--radius);
+ background: var(--panel);
+ }
}
- }
- > .clips {
- > .title {
- font-weight: bold;
- padding: 12px;
- }
+ > .clips {
+ > .title {
+ font-weight: bold;
+ padding: 12px;
+ }
- > .item {
- display: block;
- padding: 16px;
+ > .item {
+ display: block;
+ padding: 16px;
- > .description {
- padding: 8px 0;
- }
+ > .description {
+ padding: 8px 0;
+ }
- > .user {
- $height: 32px;
- padding-top: 16px;
- border-top: solid 0.5px var(--divider);
- line-height: $height;
+ > .user {
+ $height: 32px;
+ padding-top: 16px;
+ border-top: solid 0.5px var(--divider);
+ line-height: $height;
- > .avatar {
- width: $height;
- height: $height;
+ > .avatar {
+ width: $height;
+ height: $height;
+ }
}
}
}
diff --git a/src/client/pages/notifications.vue b/src/client/pages/notifications.vue
index 633718a90b..06f8ad3cba 100644
--- a/src/client/pages/notifications.vue
+++ b/src/client/pages/notifications.vue
@@ -21,6 +21,7 @@ export default defineComponent({
[symbols.PAGE_INFO]: {
title: this.$ts.notifications,
icon: 'fas fa-bell',
+ bg: 'var(--bg)',
actions: [{
text: this.$ts.markAllAsRead,
icon: 'fas fa-check',
diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue
index e7e2506020..3fb5f5f1e6 100644
--- a/src/client/pages/settings/index.vue
+++ b/src/client/pages/settings/index.vue
@@ -86,7 +86,8 @@ export default defineComponent({
setup(props, context) {
const indexInfo = {
title: i18n.locale.settings,
- icon: 'fas fa-cog'
+ icon: 'fas fa-cog',
+ bg: 'var(--bg)',
};
const INFO = ref(indexInfo);
const page = ref(props.initialPage);
diff --git a/src/client/pages/settings/other.vue b/src/client/pages/settings/other.vue
index 6857950350..21b5439041 100644
--- a/src/client/pages/settings/other.vue
+++ b/src/client/pages/settings/other.vue
@@ -26,7 +26,7 @@
<FormLink to="/bios" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>BIOS</FormLink>
<FormLink to="/cli" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>CLI</FormLink>
- <FormLink to="./delete-account"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink>
+ <FormLink to="/settings/delete-account"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink>
</FormBase>
</template>
diff --git a/src/client/pages/timeline.vue b/src/client/pages/timeline.vue
index f54549b982..9dda82462d 100644
--- a/src/client/pages/timeline.vue
+++ b/src/client/pages/timeline.vue
@@ -1,31 +1,13 @@
<template>
<div class="cmuxhskf" v-hotkey.global="keymap" v-size="{ min: [800] }">
- <XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block _isolated"/>
- <XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block _isolated" fixed/>
- <div class="tabs">
- <div class="left">
- <button class="_button tab" @click="() => { src = 'home'; saveSrc(); }" :class="{ active: src === 'home' }" v-tooltip="$ts._timelines.home"><i class="fas fa-home"></i></button>
- <button class="_button tab" @click="() => { src = 'local'; saveSrc(); }" :class="{ active: src === 'local' }" v-tooltip="$ts._timelines.local" v-if="isLocalTimelineAvailable"><i class="fas fa-comments"></i></button>
- <button class="_button tab" @click="() => { src = 'social'; saveSrc(); }" :class="{ active: src === 'social' }" v-tooltip="$ts._timelines.social" v-if="isLocalTimelineAvailable"><i class="fas fa-share-alt"></i></button>
- <button class="_button tab" @click="() => { src = 'global'; saveSrc(); }" :class="{ active: src === 'global' }" v-tooltip="$ts._timelines.global" v-if="isGlobalTimelineAvailable"><i class="fas fa-globe"></i></button>
- <span class="divider"></span>
- <button class="_button tab" @click="() => { src = 'mentions'; saveSrc(); }" :class="{ active: src === 'mentions' }" v-tooltip="$ts.mentions"><i class="fas fa-at"></i><i v-if="$i.hasUnreadMentions" class="fas fa-circle i"></i></button>
- <button class="_button tab" @click="() => { src = 'directs'; saveSrc(); }" :class="{ active: src === 'directs' }" v-tooltip="$ts.directNotes"><i class="fas fa-envelope"></i><i v-if="$i.hasUnreadSpecifiedNotes" class="fas fa-circle i"></i></button>
- </div>
- <div class="right">
- <button class="_button tab" @click="chooseChannel" :class="{ active: src === 'channel' }" v-tooltip="$ts.channel"><i class="fas fa-satellite-dish"></i><i v-if="$i.hasUnreadChannel" class="fas fa-circle i"></i></button>
- <button class="_button tab" @click="chooseAntenna" :class="{ active: src === 'antenna' }" v-tooltip="$ts.antennas"><i class="fas fa-satellite"></i><i v-if="$i.hasUnreadAntenna" class="fas fa-circle i"></i></button>
- <button class="_button tab" @click="chooseList" :class="{ active: src === 'list' }" v-tooltip="$ts.lists"><i class="fas fa-list-ul"></i></button>
- </div>
- </div>
+ <XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/>
+ <XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/>
+
<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
- <div class="tl">
+ <div class="tl _block">
<XTimeline ref="tl" class="tl"
- :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src"
+ :key="src"
:src="src"
- :list="list ? list.id : null"
- :antenna="antenna ? antenna.id : null"
- :channel="channel ? channel.id : null"
:sound="true"
@before="before()"
@after="after()"
@@ -56,19 +38,52 @@ export default defineComponent({
data() {
return {
src: 'home',
- list: null,
- antenna: null,
- channel: null,
- menuOpened: false,
queue: 0,
[symbols.PAGE_INFO]: computed(() => ({
title: this.$ts.timeline,
- subtitle: this.src === 'local' ? this.$ts._timelines.local : this.src === 'social' ? this.$ts._timelines.social : this.src === 'global' ? this.$ts._timelines.global : this.$ts._timelines.home,
icon: this.src === 'local' ? 'fas fa-comments' : this.src === 'social' ? 'fas fa-share-alt' : this.src === 'global' ? 'fas fa-globe' : 'fas fa-home',
+ bg: 'var(--bg)',
actions: [{
+ icon: 'fas fa-list-ul',
+ text: this.$ts.lists,
+ handler: this.chooseList
+ }, {
+ icon: 'fas fa-satellite',
+ text: this.$ts.antennas,
+ handler: this.chooseAntenna
+ }, {
+ icon: 'fas fa-satellite-dish',
+ text: this.$ts.channel,
+ handler: this.chooseChannel
+ }, {
icon: 'fas fa-calendar-alt',
text: this.$ts.jumpToSpecifiedDate,
handler: this.timetravel
+ }],
+ tabs: [{
+ active: this.src === 'home',
+ title: this.$ts._timelines.home,
+ icon: 'fas fa-home',
+ iconOnly: true,
+ onClick: () => { this.src = 'home'; this.saveSrc(); },
+ }, {
+ active: this.src === 'local',
+ title: this.$ts._timelines.local,
+ icon: 'fas fa-comments',
+ iconOnly: true,
+ onClick: () => { this.src = 'local'; this.saveSrc(); },
+ }, {
+ active: this.src === 'social',
+ title: this.$ts._timelines.social,
+ icon: 'fas fa-share-alt',
+ iconOnly: true,
+ onClick: () => { this.src = 'social'; this.saveSrc(); },
+ }, {
+ active: this.src === 'global',
+ title: this.$ts._timelines.global,
+ icon: 'fas fa-globe',
+ iconOnly: true,
+ onClick: () => { this.src = 'global'; this.saveSrc(); },
}]
})),
};
@@ -94,32 +109,10 @@ export default defineComponent({
src() {
this.showNav = false;
},
- list(x) {
- this.showNav = false;
- if (x != null) this.antenna = null;
- if (x != null) this.channel = null;
- },
- antenna(x) {
- this.showNav = false;
- if (x != null) this.list = null;
- if (x != null) this.channel = null;
- },
- channel(x) {
- this.showNav = false;
- if (x != null) this.antenna = null;
- if (x != null) this.list = null;
- },
},
created() {
this.src = this.$store.state.tl.src;
- if (this.src === 'list') {
- this.list = this.$store.state.tl.arg;
- } else if (this.src === 'antenna') {
- this.antenna = this.$store.state.tl.arg;
- } else if (this.src === 'channel') {
- this.channel = this.$store.state.tl.arg;
- }
},
methods: {
@@ -142,12 +135,9 @@ export default defineComponent({
async chooseList(ev) {
const lists = await os.api('users/lists/list');
const items = lists.map(list => ({
+ type: 'link',
text: list.name,
- action: () => {
- this.list = list;
- this.src = 'list';
- this.saveSrc();
- }
+ to: `/timeline/list/${list.id}`
}));
os.popupMenu(items, ev.currentTarget || ev.target);
},
@@ -155,13 +145,10 @@ export default defineComponent({
async chooseAntenna(ev) {
const antennas = await os.api('antennas/list');
const items = antennas.map(antenna => ({
+ type: 'link',
text: antenna.name,
indicate: antenna.hasUnreadNote,
- action: () => {
- this.antenna = antenna;
- this.src = 'antenna';
- this.saveSrc();
- }
+ to: `/timeline/antenna/${antenna.id}`
}));
os.popupMenu(items, ev.currentTarget || ev.target);
},
@@ -169,15 +156,10 @@ export default defineComponent({
async chooseChannel(ev) {
const channels = await os.api('channels/followed');
const items = channels.map(channel => ({
+ type: 'link',
text: channel.name,
indicate: channel.hasUnreadNote,
- action: () => {
- // NOTE: チャンネルタイムラインをこのコンポーネントで表示するようにすると投稿フォームはどうするかなどの問題が生じるのでとりあえずページ遷移で
- //this.channel = channel;
- //this.src = 'channel';
- //this.saveSrc();
- this.$router.push(`/channels/${channel.id}`);
- }
+ to: `/channels/${channel.id}`
}));
os.popupMenu(items, ev.currentTarget || ev.target);
},
@@ -185,10 +167,6 @@ export default defineComponent({
saveSrc() {
this.$store.set('tl', {
src: this.src,
- arg:
- this.src === 'list' ? this.list :
- this.src === 'antenna' ? this.antenna :
- this.channel
});
},
@@ -213,6 +191,8 @@ export default defineComponent({
<style lang="scss" scoped>
.cmuxhskf {
+ padding: var(--margin);
+
> .new {
position: sticky;
top: calc(var(--stickyTop, 0px) + 16px);
@@ -227,79 +207,15 @@ export default defineComponent({
}
}
- > .tabs {
- display: flex;
- box-sizing: border-box;
- padding: 0 8px;
- white-space: nowrap;
- overflow: auto;
- border-bottom: solid 0.5px var(--divider);
-
- // 影の都合上
- position: relative;
-
- > .right {
- margin-left: auto;
- }
-
- > .left, > .right {
- > .tab {
- position: relative;
- height: 50px;
- padding: 0 12px;
-
- &:hover {
- color: var(--fgHighlighted);
- }
-
- &.active {
- color: var(--fgHighlighted);
-
- &:after {
- content: "";
- display: block;
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- margin: 0 auto;
- width: 100%;
- height: 2px;
- background: var(--accent);
- }
- }
-
- > .i {
- position: absolute;
- top: 16px;
- right: 8px;
- color: var(--indicator);
- font-size: 8px;
- animation: blink 1s infinite;
- }
- }
-
- > .divider {
- display: inline-block;
- width: 1px;
- height: 28px;
- vertical-align: middle;
- margin: 0 8px;
- background: var(--divider);
- }
- }
+ > .tl {
+ background: var(--bg);
+ border-radius: var(--radius);
+ overflow: clip;
}
&.min-width_800px {
- > .tl {
- background: var(--bg);
- padding: 32px 0;
-
- > .tl {
- max-width: 800px;
- margin: 0 auto;
- }
- }
+ max-width: 800px;
+ margin: 0 auto;
}
}
</style>
diff --git a/src/client/pages/user-list-timeline.vue b/src/client/pages/user-list-timeline.vue
new file mode 100644
index 0000000000..491fe948c1
--- /dev/null
+++ b/src/client/pages/user-list-timeline.vue
@@ -0,0 +1,147 @@
+<template>
+<div class="eqqrhokj" v-hotkey.global="keymap" v-size="{ min: [800] }">
+ <div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
+ <div class="tl _block">
+ <XTimeline ref="tl" class="tl"
+ :key="listId"
+ src="list"
+ :list="listId"
+ :sound="true"
+ @before="before()"
+ @after="after()"
+ @queue="queueUpdated"
+ />
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent, computed } from 'vue';
+import Progress from '@client/scripts/loading';
+import XTimeline from '@client/components/timeline.vue';
+import { scroll } from '@client/scripts/scroll';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+ components: {
+ XTimeline,
+ },
+
+ props: {
+ listId: {
+ type: String,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ list: null,
+ queue: 0,
+ [symbols.PAGE_INFO]: computed(() => this.list ? {
+ title: this.list.name,
+ icon: 'fas fa-list-ul',
+ bg: 'var(--bg)',
+ actions: [{
+ icon: 'fas fa-calendar-alt',
+ text: this.$ts.jumpToSpecifiedDate,
+ handler: this.timetravel
+ }, {
+ icon: 'fas fa-cog',
+ text: this.$ts.settings,
+ handler: this.settings
+ }],
+ } : null),
+ };
+ },
+
+ computed: {
+ keymap(): any {
+ return {
+ 't': this.focus
+ };
+ },
+ },
+
+ watch: {
+ listId: {
+ async handler() {
+ this.list = await os.api('users/lists/show', {
+ listId: this.listId
+ });
+ },
+ immediate: true
+ }
+ },
+
+ methods: {
+ before() {
+ Progress.start();
+ },
+
+ after() {
+ Progress.done();
+ },
+
+ queueUpdated(q) {
+ this.queue = q;
+ },
+
+ top() {
+ scroll(this.$el, 0);
+ },
+
+ settings() {
+ this.$router.push(`/my/lists/${this.listId}`);
+ },
+
+ async timetravel() {
+ const { canceled, result: date } = await os.dialog({
+ title: this.$ts.date,
+ input: {
+ type: 'date'
+ }
+ });
+ if (canceled) return;
+
+ this.$refs.tl.timetravel(new Date(date));
+ },
+
+ focus() {
+ (this.$refs.tl as any).focus();
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.eqqrhokj {
+ padding: var(--margin);
+
+ > .new {
+ position: sticky;
+ top: calc(var(--stickyTop, 0px) + 16px);
+ z-index: 1000;
+ width: 100%;
+
+ > button {
+ display: block;
+ margin: var(--margin) auto 0 auto;
+ padding: 8px 16px;
+ border-radius: 32px;
+ }
+ }
+
+ > .tl {
+ background: var(--bg);
+ border-radius: var(--radius);
+ overflow: clip;
+ }
+
+ &.min-width_800px {
+ max-width: 800px;
+ margin: 0 auto;
+ }
+}
+</style>
diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue
index 4145c86d56..86dc7361b5 100644
--- a/src/client/pages/user/index.vue
+++ b/src/client/pages/user/index.vue
@@ -60,23 +60,9 @@
<XPhotos :user="user" :key="user.id" class="_gap"/>
</div>
<div class="main">
- <div class="nav _gap">
- <MkA :to="userPage(user)" :class="{ active: page === 'index' }" class="link">
- <i class="fas fa-comment-alt icon"></i>
- <span>{{ $ts.notes }}</span>
- </MkA>
- <MkA :to="userPage(user, 'clips')" :class="{ active: page === 'clips' }" class="link">
- <i class="fas fa-paperclip icon"></i>
- <span>{{ $ts.clips }}</span>
- </MkA>
- <MkA :to="userPage(user, 'pages')" :class="{ active: page === 'pages' }" class="link">
- <i class="fas fa-file-alt icon"></i>
- <span>{{ $ts.pages }}</span>
- </MkA>
- <div class="actions">
- <button @click="menu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button>
- <MkFollowButton v-if="!$i || $i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
- </div>
+ <div class="actions">
+ <button @click="menu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button>
+ <MkFollowButton v-if="!$i || $i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
</div>
<template v-if="page === 'index'">
<div v-if="user.pinnedNotes.length > 0" class="_gap">
@@ -178,25 +164,6 @@
</div>
<div class="contents">
- <div class="nav _gap">
- <MkA :to="userPage(user)" :class="{ active: page === 'index' }" class="link" v-click-anime>
- <i class="fas fa-comment-alt icon"></i>
- <span>{{ $ts.notes }}</span>
- </MkA>
- <MkA :to="userPage(user, 'clips')" :class="{ active: page === 'clips' }" class="link" v-click-anime>
- <i class="fas fa-paperclip icon"></i>
- <span>{{ $ts.clips }}</span>
- </MkA>
- <MkA :to="userPage(user, 'pages')" :class="{ active: page === 'pages' }" class="link" v-click-anime>
- <i class="fas fa-file-alt icon"></i>
- <span>{{ $ts.pages }}</span>
- </MkA>
- <MkA :to="userPage(user, 'gallery')" :class="{ active: page === 'gallery' }" class="link" v-click-anime>
- <i class="fas fa-icons icon"></i>
- <span>{{ $ts.gallery }}</span>
- </MkA>
- </div>
-
<template v-if="page === 'index'">
<div>
<div v-if="user.pinnedNotes.length > 0" class="_gap">
@@ -283,6 +250,27 @@ export default defineComponent({
share: {
title: this.user.name,
},
+ bg: 'var(--bg)',
+ tabs: [{
+ active: this.page === 'index',
+ title: this.$ts.overview,
+ icon: 'fas fa-home',
+ }, {
+ active: this.page === 'clips',
+ title: this.$ts.clips,
+ icon: 'fas fa-paperclip',
+ onClick: () => { this.page = 'clips'; },
+ }, {
+ active: this.page === 'pages',
+ title: this.$ts.pages,
+ icon: 'fas fa-file-alt',
+ onClick: () => { this.page = 'pages'; },
+ }, {
+ active: this.page === 'gallery',
+ title: this.$ts.gallery,
+ icon: 'fas fa-icons',
+ onClick: () => { this.page = 'gallery'; },
+ }]
} : null),
user: null,
error: null,
@@ -314,7 +302,7 @@ export default defineComponent({
mounted() {
window.requestAnimationFrame(this.parallaxLoop);
- this.narrow = this.$el.clientWidth < 1000;
+ this.narrow = true//this.$el.clientWidth < 1000;
},
beforeUnmount() {
@@ -772,37 +760,6 @@ export default defineComponent({
}
> .contents {
- > .nav {
- display: flex;
- align-items: center;
- font-size: 90%;
-
- > .link {
- flex: 1;
- display: inline-block;
- padding: 16px;
- text-align: center;
- border-bottom: solid 3px transparent;
-
- &:hover {
- text-decoration: none;
- }
-
- &.active {
- color: var(--accent);
- border-bottom-color: var(--accent);
- }
-
- &:not(.active):hover {
- color: var(--fgHighlighted);
- }
-
- > .icon {
- margin-right: 6px;
- }
- }
- }
-
> .content {
margin-bottom: var(--margin);
}
diff --git a/src/client/router.ts b/src/client/router.ts
index 225ee44e32..573f285c79 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -48,6 +48,8 @@ const defaultRoutes = [
{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
{ path: '/channels/:channelId', component: page('channel'), props: route => ({ channelId: route.params.channelId }) },
{ path: '/clips/:clipId', component: page('clip'), props: route => ({ clipId: route.params.clipId }) },
+ { path: '/timeline/list/:listId', component: page('user-list-timeline'), props: route => ({ listId: route.params.listId }) },
+ { path: '/timeline/antenna/:antennaId', component: page('antenna-timeline'), props: route => ({ antennaId: route.params.antennaId }) },
{ path: '/my/notifications', component: page('notifications') },
{ path: '/my/favorites', component: page('favorites') },
{ path: '/my/messages', component: page('messages') },
diff --git a/src/client/scripts/show-suspended-dialog.ts b/src/client/scripts/show-suspended-dialog.ts
new file mode 100644
index 0000000000..dde829cdae
--- /dev/null
+++ b/src/client/scripts/show-suspended-dialog.ts
@@ -0,0 +1,10 @@
+import * as os from '@client/os';
+import { i18n } from '@client/i18n';
+
+export function showSuspendedDialog() {
+ return os.dialog({
+ type: 'error',
+ title: i18n.locale.yourAccountSuspendedTitle,
+ text: i18n.locale.yourAccountSuspendedDescription
+ });
+}
diff --git a/src/client/style.scss b/src/client/style.scss
index 6ab5e796bd..0318013f60 100644
--- a/src/client/style.scss
+++ b/src/client/style.scss
@@ -245,7 +245,6 @@ hr {
._panel {
background: var(--panel);
border-radius: var(--radius);
- border: var(--panelBorder);
overflow: clip;
}
diff --git a/src/client/themes/_dark.json5 b/src/client/themes/_dark.json5
index ca9994d5e9..e1d5779a80 100644
--- a/src/client/themes/_dark.json5
+++ b/src/client/themes/_dark.json5
@@ -12,6 +12,7 @@
accent: '#86b300',
accentDarken: ':darken<10<@accent',
accentLighten: ':lighten<10<@accent',
+ accentedBg: ':alpha<0.15<@accent',
focus: ':alpha<0.3<@accent',
bg: '#000',
acrylicBg: ':alpha<0.5<@bg',
@@ -36,7 +37,7 @@
navFg: '@fg',
navHoverFg: ':lighten<17<@fg',
navActive: '@accent',
- navIndicator: '@accent',
+ navIndicator: '@indicator',
link: '#44a4c1',
hashtag: '#ff9156',
mention: '@accent',
diff --git a/src/client/themes/_light.json5 b/src/client/themes/_light.json5
index 973a6251f0..87895e6406 100644
--- a/src/client/themes/_light.json5
+++ b/src/client/themes/_light.json5
@@ -12,6 +12,7 @@
accent: '#86b300',
accentDarken: ':darken<10<@accent',
accentLighten: ':lighten<10<@accent',
+ accentedBg: ':alpha<0.15<@accent',
focus: ':alpha<0.3<@accent',
bg: '#fff',
acrylicBg: ':alpha<0.5<@bg',
@@ -36,7 +37,7 @@
navFg: '@fg',
navHoverFg: ':darken<17<@fg',
navActive: '@accent',
- navIndicator: '@accent',
+ navIndicator: '@indicator',
link: '#44a4c1',
hashtag: '#ff9156',
mention: '@accent',
diff --git a/src/client/ui/_common_/header.vue b/src/client/ui/_common_/header.vue
index 115f70a540..1e0db9a3a1 100644
--- a/src/client/ui/_common_/header.vue
+++ b/src/client/ui/_common_/header.vue
@@ -1,31 +1,41 @@
<template>
-<div class="fdidabkb" :class="{ center }" :style="`--height:${height};`" :key="key">
+<div class="fdidabkb" :class="{ slim: titleOnly || narrow }" :style="`--height:${height};`" :key="key">
<transition :name="$store.state.animation ? 'header' : ''" mode="out-in" appear>
<div class="buttons left" v-if="backButton">
<button class="_button button back" @click.stop="$emit('back')" @touchstart="preventDrag" v-tooltip="$ts.goBack"><i class="fas fa-chevron-left"></i></button>
</div>
</transition>
<template v-if="info">
- <div class="titleContainer">
+ <div class="titleContainer" @click="showTabsPopup">
<i v-if="info.icon" class="icon" :class="info.icon"></i>
<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/>
<div class="title">
<MkUserName v-if="info.userName" :user="info.userName" :nowrap="false" class="title"/>
<div v-else-if="info.title" class="title">{{ info.title }}</div>
- <div class="subtitle" v-if="info.subtitle">
+ <div class="subtitle" v-if="!narrow && info.subtitle">
{{ info.subtitle }}
</div>
+ <div class="subtitle activeTab" v-if="narrow && hasTabs">
+ {{ info.tabs.find(tab => tab.active)?.title }}
+ <i class="chevron fas fa-chevron-down"></i>
+ </div>
</div>
</div>
- <div class="buttons right">
- <template v-if="info.actions && showActions">
- <button v-for="action in info.actions" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag" v-tooltip="action.text"><i :class="action.icon"></i></button>
- </template>
- <button v-if="shouldShowMenu" class="_button button" @click.stop="showMenu" @touchstart="preventDrag" v-tooltip="$ts.menu"><i class="fas fa-ellipsis-h"></i></button>
- <button v-if="closeButton" class="_button button" @click.stop="$emit('close')" @touchstart="preventDrag" v-tooltip="$ts.close"><i class="fas fa-times"></i></button>
+ <div class="tabs" v-if="!narrow">
+ <button class="tab _button" v-for="tab in info.tabs" :class="{ active: tab.active }" @click="tab.onClick" v-tooltip="tab.title">
+ <i v-if="tab.icon" class="icon" :class="tab.icon"></i>
+ <span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
+ </button>
</div>
</template>
+ <div class="buttons right">
+ <template v-if="info && info.actions && !narrow">
+ <button v-for="action in info.actions" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag" v-tooltip="action.text"><i :class="action.icon"></i></button>
+ </template>
+ <button v-if="shouldShowMenu" class="_button button" @click.stop="showMenu" @touchstart="preventDrag" v-tooltip="$ts.menu"><i class="fas fa-ellipsis-h"></i></button>
+ <button v-if="closeButton" class="_button button" @click.stop="$emit('close')" @touchstart="preventDrag" v-tooltip="$ts.close"><i class="fas fa-times"></i></button>
+ </div>
</div>
</template>
@@ -52,24 +62,29 @@ export default defineComponent({
required: false,
default: false,
},
- center: {
+ titleOnly: {
type: Boolean,
required: false,
- default: true,
+ default: false,
},
},
data() {
return {
- showActions: false,
+ narrow: false,
height: 0,
key: 0,
};
},
computed: {
+ hasTabs(): boolean {
+ return this.info.tabs && this.info.tabs.length > 0;
+ },
+
shouldShowMenu() {
- if (this.info.actions != null && !this.showActions) return true;
+ if (this.info == null) return false;
+ if (this.info.actions != null && this.narrow) return true;
if (this.info.menu != null) return true;
if (this.info.share != null) return true;
if (this.menu != null) return true;
@@ -85,10 +100,10 @@ export default defineComponent({
mounted() {
this.height = this.$el.parentElement.offsetHeight + 'px';
- this.showActions = this.$el.parentElement.offsetWidth >= 500;
+ this.narrow = this.titleOnly || this.$el.parentElement.offsetWidth < 500;
new ResizeObserver((entries, observer) => {
this.height = this.$el.parentElement.offsetHeight + 'px';
- this.showActions = this.$el.parentElement.offsetWidth >= 500;
+ this.narrow = this.titleOnly || this.$el.parentElement.offsetWidth < 500;
}).observe(this.$el);
},
@@ -102,7 +117,7 @@ export default defineComponent({
showMenu(ev) {
let menu = this.info.menu ? this.info.menu() : [];
- if (!this.showActions && this.info.actions) {
+ if (this.narrow && this.info.actions) {
menu = [...this.info.actions.map(x => ({
text: x.text,
icon: x.icon,
@@ -124,6 +139,18 @@ export default defineComponent({
popupMenu(menu, ev.currentTarget || ev.target);
},
+ showTabsPopup(ev) {
+ if (!this.hasTabs) return;
+ ev.preventDefault();
+ ev.stopPropagation();
+ const menu = this.info.tabs.map(tab => ({
+ text: tab.title,
+ icon: tab.icon,
+ action: tab.onClick,
+ }));
+ popupMenu(menu, ev.currentTarget || ev.target);
+ },
+
preventDrag(ev) {
ev.stopPropagation();
}
@@ -135,7 +162,7 @@ export default defineComponent({
.fdidabkb {
display: flex;
- &.center {
+ &.slim {
text-align: center;
> .titleContainer {
@@ -190,6 +217,7 @@ export default defineComponent({
overflow: auto;
white-space: nowrap;
text-align: left;
+ font-weight: bold;
> .avatar {
$size: 32px;
@@ -219,6 +247,54 @@ export default defineComponent({
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+
+ &.activeTab {
+ text-align: center;
+
+ > .chevron {
+ display: inline-block;
+ margin-left: 6px;
+ }
+ }
+ }
+ }
+ }
+
+ > .tabs {
+ margin-left: 16px;
+ font-size: 0.8em;
+
+ > .tab {
+ display: inline-block;
+ position: relative;
+ padding: 0 10px;
+ height: 100%;
+ font-weight: normal;
+ opacity: 0.7;
+
+ &:hover {
+ opacity: 1;
+ }
+
+ &.active {
+ opacity: 1;
+
+ &:after {
+ content: "";
+ display: block;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ margin: 0 auto;
+ width: 100%;
+ height: 3px;
+ background: var(--accent);
+ }
+ }
+
+ > .icon + .title {
+ margin-left: 8px;
}
}
}
diff --git a/src/client/ui/_common_/sidebar.vue b/src/client/ui/_common_/sidebar.vue
index 333d0ac392..9817a46e30 100644
--- a/src/client/ui/_common_/sidebar.vue
+++ b/src/client/ui/_common_/sidebar.vue
@@ -11,28 +11,28 @@
<transition name="nav">
<nav class="nav" :class="{ iconOnly, hidden }" v-show="showing">
<div>
- <button class="item _button account" @click="openAccountMenu">
+ <button class="item _button account" @click="openAccountMenu" v-click-anime>
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
</button>
- <MkA class="item index" active-class="active" to="/" exact>
+ <MkA class="item index" active-class="active" to="/" exact v-click-anime>
<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
</MkA>
<template v-for="item in menu">
<div v-if="item === '-'" class="divider"></div>
- <component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to">
+ <component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime>
<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
</component>
</template>
<div class="divider"></div>
- <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance">
+ <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" v-click-anime>
<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
</MkA>
- <button class="item _button" @click="more">
+ <button class="item _button" @click="more" v-click-anime>
<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
</button>
- <MkA class="item" active-class="active" to="/settings">
+ <MkA class="item" active-class="active" to="/settings" v-click-anime>
<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
</MkA>
<button class="item _button post" @click="post">
@@ -263,24 +263,32 @@ export default defineComponent({
> .item {
padding-left: 0;
+ padding: 18px 0;
width: 100%;
text-align: center;
font-size: $ui-font-size * 1.1;
- line-height: 3.7rem;
+ line-height: initial;
> i,
> .avatar {
- margin-right: 0;
+ display: block;
+ margin: 0 auto;
}
> i {
- left: 10px;
+ opacity: 0.7;
}
> .text {
display: none;
}
+ &:hover, &.active {
+ > i, > .text {
+ opacity: 1;
+ }
+ }
+
&:first-child {
margin-bottom: 8px;
}
@@ -314,10 +322,11 @@ export default defineComponent({
height: calc(var(--vh, 1vh) * 100);
box-sizing: border-box;
overflow: auto;
+ overflow-x: clip;
background: var(--navBg);
> .divider {
- margin: 16px 0;
+ margin: 16px 16px;
border-top: solid 0.5px var(--divider);
}
@@ -326,7 +335,7 @@ export default defineComponent({
display: block;
padding-left: 24px;
font-size: $ui-font-size;
- line-height: 3rem;
+ line-height: 2.85rem;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
@@ -336,6 +345,7 @@ export default defineComponent({
color: var(--navFg);
> i {
+ position: relative;
width: 32px;
}
@@ -359,6 +369,11 @@ export default defineComponent({
animation: blink 1s infinite;
}
+ > .text {
+ position: relative;
+ font-size: 0.9em;
+ }
+
&:hover {
text-decoration: none;
color: var(--navHoverFg);
@@ -368,6 +383,23 @@ export default defineComponent({
color: var(--navActive);
}
+ &:hover, &.active {
+ &:before {
+ content: "";
+ display: block;
+ width: calc(100% - 24px);
+ height: 100%;
+ margin: auto;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-radius: 8px;
+ background: var(--accentedBg);
+ }
+ }
+
&:first-child, &:last-child {
position: sticky;
z-index: 1;
@@ -380,14 +412,38 @@ export default defineComponent({
&:first-child {
top: 0;
- margin-bottom: 16px;
- border-bottom: solid 0.5px var(--divider);
+
+ &:hover, &.active {
+ &:before {
+ content: none;
+ }
+ }
}
&:last-child {
bottom: 0;
- margin-top: 16px;
- border-top: solid 0.5px var(--divider);
+ color: var(--fgOnAccent);
+
+ &:before {
+ content: "";
+ display: block;
+ width: calc(100% - 20px);
+ height: calc(100% - 20px);
+ margin: auto;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-radius: 999px;
+ background: var(--accent);
+ }
+
+ &:hover, &.active {
+ &:before {
+ background: var(--accentLighten);
+ }
+ }
}
}
}
diff --git a/src/client/ui/default.vue b/src/client/ui/default.vue
index eef693faef..a5ec243e9e 100644
--- a/src/client/ui/default.vue
+++ b/src/client/ui/default.vue
@@ -12,7 +12,7 @@
</div>
</template>
- <main class="main" @contextmenu.stop="onContextmenu">
+ <main class="main" @contextmenu.stop="onContextmenu" :style="{ background: pageInfo?.bg }">
<header class="header" @click="onHeaderClick">
<XHeader :info="pageInfo" :back-button="true" @back="back()"/>
</header>
@@ -145,6 +145,15 @@ export default defineComponent({
}
}, '*');
}, { passive: true });
+ window.addEventListener('touchmove', ev => {
+ this.$refs.live2d.contentWindow.postMessage({
+ type: 'moveCursor',
+ body: {
+ x: ev.touches[0].clientX - iframeRect.left,
+ y: ev.touches[0].clientY - iframeRect.top,
+ }
+ }, '*');
+ }, { passive: true });
}
},
diff --git a/src/client/ui/universal.vue b/src/client/ui/universal.vue
index d6cace0f41..ec9254b697 100644
--- a/src/client/ui/universal.vue
+++ b/src/client/ui/universal.vue
@@ -2,8 +2,8 @@
<div class="mk-app" :class="{ wallpaper }">
<XSidebar ref="nav" class="sidebar"/>
- <div class="contents" ref="contents" @contextmenu.stop="onContextmenu">
- <header class="header" ref="header" @click="onHeaderClick">
+ <div class="contents" ref="contents" @contextmenu.stop="onContextmenu" :style="{ background: pageInfo?.bg }">
+ <header class="header" ref="header" @click="onHeaderClick" :style="{ background: pageInfo?.bg }">
<XHeader :info="pageInfo" :back-button="true" @back="back()"/>
</header>
<main ref="main">
@@ -258,7 +258,6 @@ export default defineComponent({
}
> .sidebar {
- border-right: solid 0.5px var(--divider);
}
> .contents {
@@ -314,6 +313,7 @@ export default defineComponent({
> .widgets {
padding: 0 var(--margin);
border-left: solid 0.5px var(--divider);
+ background: var(--bg);
@media (max-width: $widgets-hide-threshold) {
display: none;
diff --git a/src/docs/en-US/advanced/aiscript.md b/src/docs/en-US/advanced/aiscript.md
index 604d17daa8..fc2802fcd4 100644
--- a/src/docs/en-US/advanced/aiscript.md
+++ b/src/docs/en-US/advanced/aiscript.md
@@ -1,7 +1,7 @@
# AiScript
-AiScriptは、Misskeyで使用できるスクリプト言語です。
+AiScript is a scripting language for Misskey.
-<div class="info">ℹ️ AiScript実装はMisskeyとは別リポジトリで、<a href="https://github.com/syuilo/aiscript" target="_blank">オープンソースで公開されています。</a></div>
+<div class="info">ℹ️ AiScript is open source and hosted in a separate repository from Misskey. </a></div>
## 使い方
-AiScriptの構文や組み込み関数などのドキュメントは、[こちら](https://github.com/syuilo/aiscript/tree/master/docs)で公開されています。
+AiScript documentation such as syntax and built-in functions can be found [here](https://github.com/syuilo/aiscript/tree/master/docs).
diff --git a/src/docs/eo-UY/advanced/create-plugin.md b/src/docs/eo-UY/advanced/create-plugin.md
index 2a5af72917..e7826037b8 100644
--- a/src/docs/eo-UY/advanced/create-plugin.md
+++ b/src/docs/eo-UY/advanced/create-plugin.md
@@ -1,7 +1,7 @@
# プラグインの作成
Misskey Webクライアントのプラグイン機能を使うと、クライアントを拡張し、様々な機能を追加できます。 ここではプラグインの作成にあたってのメタデータ定義や、AiScript APIリファレンスを掲載します。
-## メタデータ
+## Metadatumoj
プラグインは、AiScriptのメタデータ埋め込み機能を使って、デフォルトとしてプラグインのメタデータを定義する必要があります。 メタデータは次のプロパティを含むオブジェクトです。
### name
@@ -11,7 +11,7 @@ Nomo de kromaĵo
プラグイン作者
### version
-プラグインバージョン。数値を指定してください。
+Versio de kromaĵo.数値を指定してください。
### description
プラグインの説明
diff --git a/src/docs/eo-UY/features/keyboard-shortcut.md b/src/docs/eo-UY/features/keyboard-shortcut.md
index 9a337c4fa5..b4bb35b763 100644
--- a/src/docs/eo-UY/features/keyboard-shortcut.md
+++ b/src/docs/eo-UY/features/keyboard-shortcut.md
@@ -1,15 +1,15 @@
-# キーボードショートカット
+# Fulmoklavoj
## Malloka
これらのショートカットは基本的にどこでも使えます。
<table>
<thead>
- <tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
+ <tr><th>Fulmoklavoj</th><th>効果</th><th>由来</th></tr>
</thead>
<tbody>
- <tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>新規投稿</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
+ <tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>Skribi novan noton</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
<tr><td><kbd class="key">T</kbd></td><td>タイムラインの最も新しい投稿にフォーカス</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
- <tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>通知を表示/隠す</td><td><b>N</b>otifications</td></tr>
+ <tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>Malfermi sekcio de sciigoj</td><td><b>N</b>otifications</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>Serĉi</td><td><b>S</b>earch</td></tr>
<tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>ヘルプを表示</td><td><b>H</b>elp</td></tr>
</tbody>
@@ -18,19 +18,19 @@
## 投稿にフォーカスされた状態
<table>
<thead>
- <tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
+ <tr><th>Fulmoklavoj</th><th>効果</th><th>Deveno (angla)</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">↑</kbd>, <kbd class="key">K</kbd>, <kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">Tab</kbd></kbd></td><td>上の投稿にフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key">↓</kbd>, <kbd class="key">J</kbd>, <kbd class="key">Tab</kbd></td><td>下の投稿にフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key">R</kbd></td><td>返信フォームを開く</td><td><b>R</b>eply</td></tr>
<tr><td><kbd class="key">Q</kbd></td><td>Renoteフォームを開く</td><td><b>Q</b>uote</td></tr>
- <tr><td><kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">Q</kbd></kbd></td><td>即刻Renoteする(フォームを開かずに)</td><td>-</td></tr>
+ <tr><td><kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">Q</kbd></kbd></td><td>Tuj plusendos (sen la fasado)</td><td>-</td></tr>
<tr><td><kbd class="key">E</kbd>, <kbd class="key">A</kbd>, <kbd class="key">+</kbd></td><td>リアクションフォームを開く</td><td><b>E</b>mote, re<b>A</b>ction</td></tr>
- <tr><td><kbd class="key">0</kbd>~<kbd class="key">9</kbd></td><td>数字に対応したリアクションをする(対応については後述)</td><td>-</td></tr>
+ <tr><td><kbd class="key">0</kbd>-<kbd class="key">9</kbd></td><td>数字に対応したリアクションをする(対応については後述)</td><td>-</td></tr>
<tr><td><kbd class="key">F</kbd>, <kbd class="key">B</kbd></td><td>Aldoni vian liston de preferaĵoj</td><td><b>F</b>avorite, <b>B</b>ookmark</td></tr>
- <tr><td><kbd class="key">Del</kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">D</kbd></kbd></td><td>投稿を削除</td><td><b>D</b>elete</tr>
- <tr><td><kbd class="key">M</kbd>, <kbd class="key">O</kbd></td><td>投稿に対するメニューを開く</td><td><b>M</b>ore, <b>O</b>ther</td></tr>
+ <tr><td><kbd class="key">Del</kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">D</kbd></kbd></td><td>Forviŝi la noton</td><td><b>D</b>elete</tr>
+ <tr><td><kbd class="key">M</kbd>, <kbd class="key">O</kbd></td><td>Malfelmi poŝtaĵan menuon</td><td><b>M</b>ore, <b>O</b>ther</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>CWで隠された部分を表示 or 隠す</td><td><b>S</b>how, <b>S</b>ee</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>フォーカスを外す</td><td>-</td></tr>
</tbody>
@@ -39,11 +39,11 @@
## Renoteフォーム
<table>
<thead>
- <tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
+ <tr><th>Fulmoklavoj</th><th>Efektoj</th><th>Deveno (angla)</th></tr>
</thead>
<tbody>
- <tr><td><kbd class="key">Enter</kbd></td><td>Fari renoton</td><td>-</td></tr>
- <tr><td><kbd class="key">Q</kbd></td><td>フォームを展開する</td><td><b>Q</b>uote</td></tr>
+ <tr><td><kbd class="key">Enter</kbd></td><td>Plusendi</td><td>-</td></tr>
+ <tr><td><kbd class="key">Q</kbd></td><td>Malfermi sekcio</td><td><b>Q</b>uote</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>フォームを閉じる</td><td>-</td></tr>
</tbody>
</table>
@@ -52,7 +52,7 @@
デフォルトで「👍」にフォーカスが当たっている状態です。
<table>
<thead>
- <tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
+ <tr><th>Fulmoklavoj</th><th>効果</th><th>Deveno (angla)</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">↑</kbd>, <kbd class="key">K</kbd></td><td>上のリアクションにフォーカスを移動</td><td>-</td></tr>
@@ -60,7 +60,7 @@
<tr><td><kbd class="key">←</kbd>, <kbd class="key">H</kbd>, <kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">Tab</kbd></kbd></td><td>左のリアクションにフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key">→</kbd>, <kbd class="key">L</kbd>, <kbd class="key">Tab</kbd></td><td>右のリアクションにフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key">Enter</kbd>, <kbd class="key">Space</kbd>, <kbd class="key">+</kbd></td><td>リアクション確定</td><td>-</td></tr>
- <tr><td><kbd class="key">0</kbd>~<kbd class="key">9</kbd></td><td>数字に対応したリアクションで確定</td><td>-</td></tr>
+ <tr><td><kbd class="key">0</kbd>-<kbd class="key">9</kbd></td><td>数字に対応したリアクションで確定</td><td>-</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>リアクションするのをやめる</td><td>-</td></tr>
</tbody>
</table>
diff --git a/src/docs/eo-UY/features/mfm.md b/src/docs/eo-UY/features/mfm.md
index cca995657d..2970fbd7b2 100644
--- a/src/docs/eo-UY/features/mfm.md
+++ b/src/docs/eo-UY/features/mfm.md
@@ -5,8 +5,8 @@ MFMは、Misskey Flavored Markdownの略で、Misskeyの様々な場所で使用
- ノート本文
- CW注釈
- Nomo de uzanto
-- ユーザーの自己紹介
+- Profilo de uzanto
-## 開発者向け情報
+## Informoj por programistoj
MFMのパーサー実装はライブラリとして公開されており、簡単にクライアントにMFMを組み込むことが可能です。
- [misskey-dev/mfm.js](https://github.com/misskey-dev/mfm.js) - JavaScriptパーサー実装
diff --git a/src/docs/eo-UY/features/note.md b/src/docs/eo-UY/features/note.md
index 66c03c777a..aa72981e24 100644
--- a/src/docs/eo-UY/features/note.md
+++ b/src/docs/eo-UY/features/note.md
@@ -12,7 +12,7 @@
<div class="info">ℹ️ コンピューターのクリップボードに画像データがある状態で、フォーム内のテキストボックスにペーストするとその画像を添付することができます。</div>
<div class="info">ℹ️ テキストボックス内で<kbd class="key">Ctrl + Enter</kbd>を押すことでも投稿できます。</div>
-## Fari renoton
+## Plusendi la noton
既にあるノートを引用、もしくはそのノートを新しいノートとして共有する行為、またそれによって作成されたノートをRenoteと呼びます。 自分がフォローしているユーザーの、気に入ったノートを自分のフォロワーに共有したい場合や、過去の自分のノートを再度共有したい場合に使います。 同じノートに対して無制限にRenoteを行うことができますが、あまり連続して使用すると迷惑になる場合もあるので、注意しましょう。
<div class="warn">⚠️ 公開範囲がフォロワーやダイレクトのノートはRenoteできません</div>
diff --git a/src/docs/eo-UY/features/timeline.md b/src/docs/eo-UY/features/timeline.md
index 7c0e8dfd7f..72ee7a5d76 100644
--- a/src/docs/eo-UY/features/timeline.md
+++ b/src/docs/eo-UY/features/timeline.md
@@ -14,18 +14,18 @@
全てのローカルユーザーの「ホーム」指定されていない投稿と、サーバーに届いた全てのリモートユーザーの「ホーム」指定されていない投稿が流れます。GTLと略されます。
## 比較
-| ソース | | | Templinio | | |
-| ------------ | --------- | ----- | --------- | ------- | ------- |
-| Uzantoj | Videbleco | Hejma | Loka | Sociala | Malloka |
-| ローカル (フォロー) | Publikigi | ✔ | ✔ | ✔ | ✔ |
-| | Hejma | ✔ | | ✔ | |
-| | Sekvantoj | ✔ | ✔ | ✔ | ✔ |
-| リモート (フォロー) | Publikigi | ✔ | | ✔ | ✔ |
-| | Hejma | ✔ | | ✔ | |
-| | Sekvantoj | ✔ | | ✔ | ✔ |
-| ローカル (未フォロー) | Publikigi | | ✔ | ✔ | ✔ |
-| | Hejma | | | | |
-| | Sekvantoj | | | | |
-| リモート (未フォロー) | Publikigi | | | | ✔ |
-| | Hejma | | | | |
-| | Sekvantoj | | | | |
+| ソース | | | Templinio | | |
+| --------------------- | --------- | ----- | --------- | ------- | ------- |
+| Uzantoj | Videbleco | Hejma | Loka | Sociala | Malloka |
+| Lokaj (sekvataj) | Publika | ✔ | ✔ | ✔ | ✔ |
+| | Nur hejma | ✔ | | ✔ | |
+| | Sekvantoj | ✔ | ✔ | ✔ | ✔ |
+| Transaj (sekvataj) | Publika | ✔ | | ✔ | ✔ |
+| | Nur hejma | ✔ | | ✔ | |
+| | Sekvantoj | ✔ | | ✔ | ✔ |
+| Lokaj (ne sekvataj) | Publika | | ✔ | ✔ | ✔ |
+| | Nur hejma | | | | |
+| | Sekvantoj | | | | |
+| Transaj (ne sekvataj) | Publika | | | | ✔ |
+| | Nur hejma | | | | |
+| | Sekvantoj | | | | |
diff --git a/src/docs/eo-UY/general/glossary.md b/src/docs/eo-UY/general/glossary.md
index b590714097..fe3b034181 100644
--- a/src/docs/eo-UY/general/glossary.md
+++ b/src/docs/eo-UY/general/glossary.md
@@ -10,7 +10,7 @@ Misskeyに関する用語集です。
## API
(読み: えーぴーあい) Misskeyのサーバーが公開している、プログラムからMisskeyを扱うためのインターフェース。詳細は[こちら。](../advanced/api)
-## Bot
+## Roboto
(読み: ぼっと) プログラムによって動作しているアカウント。
## CW
@@ -19,14 +19,14 @@ Misskeyに関する用語集です。
## Fediverso
(読み: ふぇでぃばーす) Misskeyを含む様々な分散型ソフトウェアのサーバーで構成されたネットワーク。
-## MTL
-An abbreviation for "Malloka TempLinio".タイムラインの詳細は[こちら。](../features/timeline)
+## MTL (GTL)
+Kaplitero de "Malloka TempLinio".タイムラインの詳細は[こちら。](../features/timeline)
## HTL
-ホームタイムライン(Home TimeLine)の略。タイムラインの詳細は[こちら。](../features/timeline)
+Kaplitero de "Hejma TempLinio".タイムラインの詳細は[こちら。](../features/timeline)
## LTL
-An abbreviation for "Loka TempLinio".タイムラインの詳細は[こちら。](../features/timeline)
+Kaplitero de "Loka TempLinio".タイムラインの詳細は[こちら。](../features/timeline)
## MFM
(読み: えむえふえむ) Misskey Flavored Markdownの略で、Misskey上で使用できるマークアップ言語です。詳細は[こちら。](../features/mfm)
@@ -34,25 +34,25 @@ An abbreviation for "Loka TempLinio".タイムラインの詳細は[こちら。
## NSFW
(読み: のっとせーふふぉーわーく) Not Safe For Workの略。画像を「閲覧注意」扱いにし、操作なしには表示しないようにすることができる機能。
-## Renoto
-(読み: りのーと) 既にあるノートを引用、もしくはそのノートを新しいノートとして共有する行為、またそれによって作成されたノート。詳細は[こちら。](../features/note)
+## Notoj plusenditaj
+(読み: りのーと) 既にあるノートを引用、もしくはそのノートを新しいノートとして共有する行為、またそれによって作成されたノート。Rigardu por sciu pli tie[.](../features/note)
## STL
-An abbreviation for "Sociala TempLinio".タイムラインの詳細は[こちら。](../features/timeline)
+Kaplitero de "Sociala TempLinio".Por sciu pri la templinio, rigardu tie[.](../features/timeline)
## Ai
Ai estas oficiala maskoto de Misskey.
-## Aktivaj Uzantoj:
+## Aktiva uzanto
インスタンスにアカウントを作っているユーザーのうち、現在も実際にサービスを利用しているユーザーのこと。
## Nodo
todo
-## Ŝaltpodio
+## Personecigitaj emoĵioj
サーバーで用意された絵文字。カスタム絵文字ではない通常の絵文字は「Unicode絵文字」と区別して呼ばれる。
-## コントロールパネル
+## Ŝaltpodio
インスタンスの設定画面のこと。
## Servilo
@@ -61,25 +61,25 @@ todo
## Mutigi
ノートをパブリックな公開範囲で投稿できなくされている状態。モデレーターの判断でユーザーごとに設定されます。詳細は[こちら。](../features/silence)
-## Disko
+## ジョブキュー
アクティビティ配送などを順番に行うためのシステム。
-## Flostigi
+## Flostigita
アカウントが使用不可に設定されている状態。
-## Miskiisto
+## Disko
Misskeyにアップロードしたファイルを管理する機能。詳細は[こちら。](../features/drive)
## Notoj
-Misskeyに投稿される、文章、ファイル、アンケートなどを含めることができるコンテンツ。詳細は[こちら。](../features/note)
+Misskeyに投稿される、文章、ファイル、アンケートなどを含めることができるコンテンツ。Rigardu por sciu pli tie[.](../features/note)
## Miskiisto
-Misskeyを使う人のこと。
+Uzuloj de Misskey.
## Kontrolisto
スパムの凍結およびサイレンスや不適切な投稿の削除など、コミュニティ運営に関する権限を持つユーザー。
-## Transa/fora
+## Transa, Surloka
他サーバーのことを指します。リモートユーザーといったように接頭辞としても使われます。ローカルの逆です。
## Kunfederado
diff --git a/src/docs/eo-UY/general/links.md b/src/docs/eo-UY/general/links.md
index 8bc76cc433..5d5c8dc7d9 100644
--- a/src/docs/eo-UY/general/links.md
+++ b/src/docs/eo-UY/general/links.md
@@ -1,7 +1,7 @@
# リンク集
## Webサイト
-- [Oficiala Discord](https://discord.gg/Wp8gVStHW3) - Servilo Discord'a oficiala de Misskey
+- [Oficiala Discord](https://discord.gg/Wp8gVStHW3) - la Servilo Discord'a oficiala de Misskey
- [Misskey Forum](https://forum.misskey.io/) - Misskeyに関する話題を扱うフォーラム
## Kontoj
diff --git a/src/docs/eo-UY/general/misskey.md b/src/docs/eo-UY/general/misskey.md
index cafd053335..506f7a8f24 100644
--- a/src/docs/eo-UY/general/misskey.md
+++ b/src/docs/eo-UY/general/misskey.md
@@ -1,6 +1,6 @@
# Pri Misskey
-Misskey estas malfermitkoda distribuita mikroblogo. Ĝia trajtoj estas diversaj funkcioj je disko aŭ reagoj ktp, kaj alte agordebla fasado. Evoluigo ekfaris de syuilo de 2014.
+Misskey estas malfermitkoda distribuita mikroblogo. Ĝi enhavas diversaj funkcioj ekzemple disko, reagoj, ktp kaj alte agordebla fasado. Evoluigo ekfaris de syuilo de 2014.
## Historio
開発当初は掲示板がメインのサービスでしたが、ユーザーが短文を投稿し、それを時系列で流れるタイムライン機能を追加したところ人気が高まり、徐々にそれがメインとして開発が進むようになりました。 当初は分散型ではありませんでしたが、2018年にActivityPubを実装し分散型になったことで、より多くの方に認知され利用されるサービスになり、現在に至ります。
diff --git a/src/docs/eo-UY/general/troubleshooting.md b/src/docs/eo-UY/general/troubleshooting.md
index f895b49847..d7edc154e3 100644
--- a/src/docs/eo-UY/general/troubleshooting.md
+++ b/src/docs/eo-UY/general/troubleshooting.md
@@ -1,4 +1,4 @@
-# トラブルシューティング
+# Problemsolvi
<div class="info">ℹ️ <a href="./faq">よくある質問</a>も合わせてお役立てください。</div>
問題が発生したときは、まずこちらをご確認ください。 該当する項目が無い、もしくは手順を試しても効果がない場合は、サーバーの管理者に連絡するか[不具合を報告](./report-issue)してください。
diff --git a/src/docs/ko-KR/admin/disable-timelines.md b/src/docs/ko-KR/admin/disable-timelines.md
index b081e35ab0..00c6ca4d69 100644
--- a/src/docs/ko-KR/admin/disable-timelines.md
+++ b/src/docs/ko-KR/admin/disable-timelines.md
@@ -1,8 +1,8 @@
-# LTL/STL/GTLの無効化
-Misskeyでは、LTL/STL/GTLをそれぞれ無効化することができます。有効/無効を切り替えるには、インスタンスコントロールパネルで設定します。
+# 타임라인의 비활성화
+Misskey에서는 로컬, 소셜, 글로벌 타임라인을 각각 비활성화할 수 있습니다. 활성화 유무는 인스턴스 설정에서 제어판에서 설정할 수 있습니다.
-LTLやSTLは、そのインスタンス全員の投稿が見れるため、新規のユーザーにとってはユーザーを探す必要がなくなり、興味のあるユーザーを見つけやすいという利点があります。 しかし同時に、フォロー機能が活用されなくなったり、不適切な投稿が目につきやすくなったり、チャットのようになることで内輪感が生じて逆に新規ユーザーが参加しにくくなるといったデメリットも持ち合わせています。 サーバーによってメリット/デメリットどちらが優勢かは異なるので、オプションとして無効にできるようになっています。 もしデメリットの方が上回っていると感じたら、それらのタイムラインを無効化することも検討してください。
+로컬 타임라인이나 소셜 타임라인에서는 인스턴스에 있는 모든 유저의 게시물을 볼 수 있으므로, 신규 유저가 다른 유저를 찾고, 관심사가 비슷한 유저를 찾기 쉽다는 장점이 있습니다. 그러나, 이런 특징 때문에 팔로우 기능의 활용이 저하되거나, 부적절한 게시물이 노출되기 쉬워지고, 타임라인 대화로 인해 인스턴스가 하나의 친목 커뮤니티화되는 경우 신규 유저의 진입 장벽으로 작용할 수 있는 등의 단점 또한 존재합니다. 각 타임라인의 특성이 인스턴스에 어떠한 영향을 주는 지는 인스턴스의 특성에 따라 다르므로, 설정을 통해 활성화 또는 비활성화할 수 있도록 설계되어 있습니다. 만약 장점에 비해 단점이 더 눈에 띄는 경우, 각각의 타임라인을 비활성화하는 것을 검토해 보시기 바랍니다.
-<div class="warn">⚠️ 無効化を行うと、ユーザーが困惑し、短期的に見て利用者が減る可能性があります。そのため、無効化の際は影響を慎重に検討し、事前に説明してフォローを整える期間を一定程度設けることを推奨します。</div>
+<div class="warn">⚠️ 타임라인을 비활성화할 경우, 유저들의 혼란을 불러 일으키며 단기적으로 이용자가 감소할 수 있습니다. 따라서, 비활성화를 결정하기 이전에 영향에 대해 신중히 검토하고, 사전에 유저에게 공지하고 일정 기간 유예기간을 두어 유저 간 팔로우 관계가 형성되도록 하는 것을 권장합니다.</div>
-なお、管理者/モデレーターは、これらのタイムラインの無効化状態は適用されず、引き続き利用することが可能です。
+또한, 관리자 및 모더레이터는 타임라인 비활성화 유무에 상관없이 각각의 타임라인을 이용할 수 있습니다.
diff --git a/src/docs/ko-KR/admin/faq.md b/src/docs/ko-KR/admin/faq.md
index 317b4e0655..2b3d148d6d 100644
--- a/src/docs/ko-KR/admin/faq.md
+++ b/src/docs/ko-KR/admin/faq.md
@@ -1,5 +1,5 @@
-# よくある質問
-ここでは、サーバー管理者向けのよくある質問を掲載しています。
+# 자주 묻는 질문
+여기에서는 서버 운영에 관련해서 자주 묻는 질문에 대해 다룹니다.
-## デフォルトテーマを設定したい
-現在、デフォルトテーマ設定機能は実装されていません。
+## 기본 테마를 설정하고 싶어요
+현재 기본 테마를 설정하는 기능은 구현되어 있지 않습니다.
diff --git a/src/docs/ko-KR/advanced/aiscript.md b/src/docs/ko-KR/advanced/aiscript.md
index 604d17daa8..c4f21b79cc 100644
--- a/src/docs/ko-KR/advanced/aiscript.md
+++ b/src/docs/ko-KR/advanced/aiscript.md
@@ -1,7 +1,7 @@
# AiScript
-AiScriptは、Misskeyで使用できるスクリプト言語です。
+AiScript는, Misskey에서 사용할 수 있는 스크립트 언어입니다.
-<div class="info">ℹ️ AiScript実装はMisskeyとは別リポジトリで、<a href="https://github.com/syuilo/aiscript" target="_blank">オープンソースで公開されています。</a></div>
+<div class="info">ℹ️ AiScript 기능은 Misskey와 별도 리포지토리에서 <a href="https://github.com/syuilo/aiscript" target="_blank">오픈소스로 공개하고 있습니다.</a></div>
-## 使い方
-AiScriptの構文や組み込み関数などのドキュメントは、[こちら](https://github.com/syuilo/aiscript/tree/master/docs)で公開されています。
+## 사용법
+AiScript의 구문이나 내장 함수에 대한 문서는 [여기에서](https://github.com/syuilo/aiscript/tree/master/docs) 확인할 수 있습니다.
diff --git a/src/docs/ko-KR/general/apps.md b/src/docs/ko-KR/general/apps.md
index 1f4c85fe8f..a71f377483 100644
--- a/src/docs/ko-KR/general/apps.md
+++ b/src/docs/ko-KR/general/apps.md
@@ -1,6 +1,6 @@
-# サードパーティアプリのリスト
-## クライアント
+# 서드파티 어플리케이션 목록
+## 클라이언트
todo
-## 連携サービス
+## 연동 서비스
todo
diff --git a/src/docs/ko-KR/general/changelog.md b/src/docs/ko-KR/general/changelog.md
index 6766a63b20..f850f5c7b7 100644
--- a/src/docs/ko-KR/general/changelog.md
+++ b/src/docs/ko-KR/general/changelog.md
@@ -1,5 +1,5 @@
-# 更新履歴
-<div class="info">ℹ️ このサーバーの更新履歴です。Misskeyの最新のリリースについては、<a href="https://github.com/misskey-dev/misskey/blob/master/CHANGELOG.md" target="_blank">GitHub</a>をご確認ください。</div>
+# 업데이트 정보
+<div class="info">ℹ️ 이 서버의 업데이트 정보입니다. Misskey 최신 버전의 업데이트 정보는 <a href="https://github.com/misskey-dev/misskey/blob/master/CHANGELOG.md" target="_blank">Github에서</a> 확인할 수 있습니다.</div>
<!-- For translators: Do not edit these comments. -->
<!--[CHANGELOG]-->
diff --git a/src/docs/ko-KR/general/faq.md b/src/docs/ko-KR/general/faq.md
index c272b2ad42..ed858b0e54 100644
--- a/src/docs/ko-KR/general/faq.md
+++ b/src/docs/ko-KR/general/faq.md
@@ -1,28 +1,28 @@
-# よくある質問
-ここでは利用上のよくある質問について掲載しています。 Misskeyのプロジェクト自体についてのよくある質問は[こちら](./misskey)に掲載されています。
+# 자주 묻는 질문
+여기에서는 Misskey 이용에 대해서 자주 묻는 질문에 대해 다룹니다. Misskey 프로젝트 자체에 대한 질문은 [여기에서](./misskey) 다루고 있습니다.
-## iOS/Androidのアプリはありますか?
-公式にはそういったOSのネイティブアプリを開発していませんが、サードパーティ製のアプリがいくつかあります。 詳しくは[こちら](./apps)をご覧ください。
+## iOS/Android용 어플리케이션이 있나요?
+공식 어플리케이션은 없지만, 비공식 서드파티 어플리케이션을 사용하실 수 있습니다. 자세한 사항은 [여기를](./apps) 참조해 주세요.
-ただ、サードパーティ製アプリはどうしても機能への対応が遅れてしまうため、とくに拘りがなければ公式のWebクライアントの利用をおすすめします。 なお、MisskeyのWebクライアントはPWAに対応しているので、ネイティブアプリのように動作させることも可能です。 詳しくは[こちら](todo)をご覧ください。
+단, 비공식 어플리케이션의 경우 최신 기능을 사용할 수 있기까지 시간이 걸릴 수 있으므로, 대부분의 경우 공식 Web 클라이언트를 사용하시는 것을 추천드립니다. 또한, Misskey의 Web 클라이언트는 PWA를 지원하여, 브라우저에서 지원하는 경우 네이티브 앱처럼 사용할 수 있습니다. 자세한 사항은 [여기를](todo) 참조해 주세요.
-## Mastodonクライアントでログインできないのですが?
-MisskeyはMastodonのAPIと互換性がないため、一部を除きMastodonクライアントでMisskeyを利用することはできません。
+## Mastodon 클라이언트로는 로그인할 수 없나요?
+Misskey는 Mastdon의 API와 호환성이 없기 때문에, 일부를 제외한 Mastodon 클라이언트에서는 Misskey를 이용하실 수 없습니다.
-## 他のサーバーのユーザーをフォローするときは?
-メニューから検索を選び、ユーザー名をホスト込みで入力します。例: `@syuilo@misskey.io`
+## 다른 서버의 유저를 팔로우하고 싶어요
+메뉴에서 검색을 선택해서, 유저명과 호스트(도메인)을 입력하세요. 예시: `@syuilo@misskey.io`
-## Renoteを削除するには?
-Renoteの時刻表示の隣にある「...」を押し、「Renote解除」を選択します。 Renoteについては[こちら](../features/note)をご確認ください。
+## Renote를 취소할래요
+Renote한 시간 옆에 있는 '...'를 누르고, "Renote 취소"를 선택하세요. Renote에 대해서는 [여기를](../features/note) 참조해 주세요.
-## URLのプレビューを表示させたくない
-MFMには、そのURLのプレビューを無効にする構文があります。詳細は[MFMチートシート](/mfm-cheat-sheet)をご確認ください。
+## URL의 미리보기를 숨기고 싶어요
+MFM를 이용하여 URL의 미리보기를 숨길 수 있습니다. 자세한 사항은 [MFM 도움말](/mfm-cheat-sheet)을 확인해 주세요.
-## カスタム絵文字を追加したい
-運営者のみがカスタム絵文字を追加、編集、削除できます。それらを希望する場合は運営者に依頼してください。
+## 커스텀 이모지를 추가하고 싶어요
+커스텀 이모지의 추가, 편집 및 삭제는 운영자만 할 수 있습니다. 커스텀 이모지에 대해서는 각 인스턴스의 운영자에게 문의해 주세요.
-## Botを開発したい
-Misskey APIを利用してBotの開発が可能です。[こちら](../advanced/develop-bot)をご確認ください。
+## Bot을 개발하고 싶어요
+Misskey API를 이용하여 Bot을 개발할 수 있습니다. 자세한 사항은 [여기를](../advanced/develop-bot) 참조해 주세요.
-## ノートの翻訳機能はどのサービスを使用していますか?
-[DeepL](https://www.deepl.com/)を使用しています。
+## 노트 번역 기능은 어떤 서비스를 사용하나요?
+노트 번역에는 [DeepL](https://www.deepl.com/)을 사용하고 있습니다.
diff --git a/src/docs/ko-KR/general/glossary.md b/src/docs/ko-KR/general/glossary.md
index 09ab838a39..26634cca09 100644
--- a/src/docs/ko-KR/general/glossary.md
+++ b/src/docs/ko-KR/general/glossary.md
@@ -1,89 +1,89 @@
-# 用語集
-Misskeyに関する用語集です。
+# 용어 사전
+Misskey에서 사용하는 용어 모음입니다.
## ActivityPub
-(読み: あくてぃびてぃぱぶ) 分散型を実現するために用いられるプロトコル(仕様)。このプロトコルに則ってサーバー同士通信を行うことで、連合が行われ、Fediverseを形成しています。
+분산형 서비스를 구현하기 위해 사용되는 프로토콜(통신 규약). 이 프로토콜을 통해 다른 서버와 통신하여 연합을 이루고, 이들이 모여 연합우주(Fediverse)를 이룹니다.
## AiScript
-(読み: あいすくりぷと) Misskey上で使用できるプログラミング言語です。詳細は[こちら。](../advanced/aiscript)
+아이스크립트. Misskey에서 사용할 수 있는 프로그래밍 언어. 자세한 내용은 [여기로](../advanced/aiscript)
## API
-(読み: えーぴーあい) Misskeyのサーバーが公開している、プログラムからMisskeyを扱うためのインターフェース。詳細は[こちら。](../advanced/api)
+에이-피-아이. 프로그램이 Misskey와 상호 작용하는 데에 사용하는 인터페이스. 자세한 내용은 [여기로](../advanced/api)
-## Bot
-(読み: ぼっと) プログラムによって動作しているアカウント。
+## Bot (봇)
+프로그램 또는 기타 자동화된 수단으로 운영되는 계정.
## CW
-(読み: こんてんつわーにんぐ) Contents Warningの略。ノートの内容を、操作なしには表示しないようにできる機能。主に長大な内容を隠すためや、ネタバレ防止などに使われます。
+'Contents Warning'의 줄임말. 제목를 설정하여 노트의 내용을 숨기는 기능. 본문이 길 때, 또는 스포일러 등 상대방이 불쾌해할 수 있는 내용을 감추는 데에 주로 사용합니다.
-## Fediverse
-(読み: ふぇでぃばーす) Misskeyを含む様々な分散型ソフトウェアのサーバーで構成されたネットワーク。
+## 연합우주 (Fediverse)
+Misskey를 비롯한 분산형 서비스 사이의 통신 및 정보 교환으로 이루어지는 네트워크.
## GTL
-グローバルタイムライン(Global TimeLine)の略。タイムラインの詳細は[こちら。](../features/timeline)
+글로벌 타임라인(Global TimeLine)의 줄임말. 타임라인에 대한 자세한 내용은 [여기로.](../features/timeline)
## HTL
-ホームタイムライン(Home TimeLine)の略。タイムラインの詳細は[こちら。](../features/timeline)
+홈 타임라인(Home TimeLine)의 줄임말. 타임라인에 대한 자세한 내용은 [여기로.](../features/timeline)
## LTL
-ローカルタイムライン(Local TimeLine)の略。タイムラインの詳細は[こちら。](../features/timeline)
+로컬 타임라인(Local TimeLine)의 줄임말. 타임라인에 대한 자세한 내용은 [여기로.](../features/timeline)
## MFM
-(読み: えむえふえむ) Misskey Flavored Markdownの略で、Misskey上で使用できるマークアップ言語です。詳細は[こちら。](../features/mfm)
+Misskey Flavored Markdown의 줄임말. Misskey에서 사용할 수 있는 마크업 언어. 자세한 사항은 [여기로.](../features/mfm)
## NSFW
-(読み: のっとせーふふぉーわーく) Not Safe For Workの略。画像を「閲覧注意」扱いにし、操作なしには表示しないようにすることができる機能。
+Not Safe For Work의 줄임말. 열람 시 주의가 필요한 사진을 감추어, 바로 표시되지 않게 하는 기능.
## Renote
-(読み: りのーと) 既にあるノートを引用、もしくはそのノートを新しいノートとして共有する行為、またそれによって作成されたノート。詳細は[こちら。](../features/note)
+이미 존재하는 노트를 인용, 또는 새로 작성한 노트처럼 타임라인에 공유하는 것. 또는 이를 통해 새로 작성된 노트를 가리키는 말. 자세한 사항은 [여기로.](../features/note)
## STL
-ソーシャルタイムライン(Social TimeLine)の略。タイムラインの詳細は[こちら。](../features/timeline)
+소셜 타임라인(Social TimeLine)의 줄임말. 타임라인에 대한 자세한 내용은 [여기로.](../features/timeline)
-## 藍
-(読み: あい) Misskeyの看板娘(公式キャラクター)です。
+## 아이(Ai)
+Misskey의 공식 마스코트입니다.
-## アクティブユーザー
-インスタンスにアカウントを作っているユーザーのうち、現在も実際にサービスを利用しているユーザーのこと。
+## 활성 사용자
+인스턴스에 계정을 생성한 유저 중에서, 현재 실질적으로 서비스를 이용 중인 사용자를 일컫는 말.
## 인스턴스
todo
## 커스텀 이모지
-サーバーで用意された絵文字。カスタム絵文字ではない通常の絵文字は「Unicode絵文字」と区別して呼ばれる。
+서버 자체적으로 사용할 수 있는 이모지. 커스텀 이모지 이외의 일반적인 이모지는 '유니코드 이모지'로 불린다.
-## コントロールパネル
-インスタンスの設定画面のこと。
+## 제어판
+인스턴스 설정 화면을 가리키는 말.
## 서버
todo
## 사일런스
-ノートをパブリックな公開範囲で投稿できなくされている状態。モデレーターの判断でユーザーごとに設定されます。詳細は[こちら。](../features/silence)
+노트를 공개 범위로 게시할 수 없게 된 상태. 관리자가 유저별로 설정할 수 있습니다. 자세한 사항은 [여기로.](../features/silence)
## 작업 대기열
-アクティビティ配送などを順番に行うためのシステム。
+액티비티 전송 등의 작업을 차례대로 수행하기 위한 시스템.
## 정지
-アカウントが使用不可に設定されている状態。
+계정을 사용할 수 없게 된 상태.
## 드라이브
-Misskeyにアップロードしたファイルを管理する機能。詳細は[こちら。](../features/drive)
+Misskey에 업로드된 파일을 관리하는 기능. 자세한 사항은 [여기로.](../features/drive)
## 노트
-Misskeyに投稿される、文章、ファイル、アンケートなどを含めることができるコンテンツ。詳細は[こちら。](../features/note)
+Misskey에서 글, 파일, 투표 등을 포함하여 작성할 수 있는 콘텐츠. 자세한 사항은 [여기로.](../features/note)
-## ミスキスト
-Misskeyを使う人のこと。
+## 미스키스트 (Misskist)
+미스키를 사용하는 사람을 일컫는 말.
## 모더레이터
-スパムの凍結およびサイレンスや不適切な投稿の削除など、コミュニティ運営に関する権限を持つユーザー。
+스팸 계정의 정지 및 사일런스, 부적절한 게시물을 삭제하는 등 커뮤니티 운영에 대한 권한을 가진 유저.
## 리모트
-他サーバーのことを指します。リモートユーザーといったように接頭辞としても使われます。ローカルの逆です。
+자신이 속한 서버와 구별하여, 다른 서버를 가리키는 말. '리모트 유저'와 같이 접두사처럼 사용하기도 합니다. 반대말로는 '로컬'이 있습니다.
## 연합
-サーバー上で作成された情報が他のサーバーに伝わること。
+서버에서 작성된 정보가 다른 서버로 전달되는 것을 이르는 말.
## 로컬
-自サーバーのことを指します。ローカルユーザー、ローカルタイムラインといったように接頭辞としても使われます。リモートの逆です。
+자신이 속한 서버를 가리키는 말. '로컬 유저', '로컬 타임라인'과 같이 접두사처럼 사용하기도 합니다. 반대말로는 '로컬'이 있습니다.
diff --git a/src/docs/ko-KR/general/links.md b/src/docs/ko-KR/general/links.md
index 0ae050b0ed..12c5f8c8b4 100644
--- a/src/docs/ko-KR/general/links.md
+++ b/src/docs/ko-KR/general/links.md
@@ -1,12 +1,12 @@
-# リンク集
+# 참고할 만한 링크
-## Webサイト
-- [Official Discord](https://discord.gg/Wp8gVStHW3) - Misskey公式Discordサーバー
-- [Misskey Forum](https://forum.misskey.io/) - Misskeyに関する話題を扱うフォーラム
+## 웹 사이트
+- [Official Discord](https://discord.gg/Wp8gVStHW3) - Misskey 공식 Discord 서버
+- [Misskey Forum](https://forum.misskey.io/) - Misskey에 대한 이야기를 나누는 포럼
## 계정
-- [@repo@misskey.io](https://misskey.io/@repo) - Misskeyのリポジトリの更新を投稿するbot
+- [@repo@misskey.io](https://misskey.io/@repo) - Misskey 리포지터리의 변경 사항을 게시하는 Bot
-## ライブラリ
-- [misskey-dev/misskey.js](https://github.com/misskey-dev/misskey.js) - JavaScriptのMisskey SDK
-- [misskey-dev/mfm.js](https://github.com/misskey-dev/mfm.js) - JavaScriptのMFMパーサー実装
+## 라이브러리
+- [misskey-dev/misskey.js](https://github.com/misskey-dev/misskey.js) - Javascript용 Misskey SDK
+- [misskey-dev/mfm.js](https://github.com/misskey-dev/mfm.js) - Javascript 기반 MFM 구문 해석기
diff --git a/src/docs/ko-KR/general/misskey.md b/src/docs/ko-KR/general/misskey.md
index d35e2124ea..decd983e7d 100644
--- a/src/docs/ko-KR/general/misskey.md
+++ b/src/docs/ko-KR/general/misskey.md
@@ -1,16 +1,16 @@
# Misskey에 대하여
-Misskeyはオープンソースの分散型マイクロブログプラットフォームプロジェクトです。 開発は日本でsyuiloによって2014年から開始されました。 ドライブ、リアクションなどの豊富な機能や、高いカスタマイズ性を備えたUIを持つことが特徴です。
+Misskey는 오픈소스 분산형 마이크로블로깅 플랫폼 프로젝트입니다. 개발은 2014년부터 일본의 syuilo의 주도로 시작되었습니다. 드라이브, 리액션 등의 풍부한 기능과, 다양한 커스터마이징 기능을 가진 UI를 가진 것이 특징입니다.
-## 歴史
-開発当初は掲示板がメインのサービスでしたが、ユーザーが短文を投稿し、それを時系列で流れるタイムライン機能を追加したところ人気が高まり、徐々にそれがメインとして開発が進むようになりました。 当初は分散型ではありませんでしたが、2018年にActivityPubを実装し分散型になったことで、より多くの方に認知され利用されるサービスになり、現在に至ります。
-<div class="info">ℹ️ Misskeyという名前は、syuiloが当時聴いていたMay'nというアーティストの楽曲、Brain Diverの歌詞に由来します。</div>
+## 역사
+개발 초기에는 게시판 형태의 서비스였으나, 유저가 게시한 짧은 글이 시간 순서대로 흘러가는 타임라인 기능을 추가함으로 인기를 받게 되어, 이후 이를 중심으로 개발이 진행되었습니다. 초기에는 분산형이 아니었지만, 2018년에 ActivityPub 프로토콜을 지원함으로써 분산형 SNS로 발돋움하여 널리 알려지게 되었습니다.
+<div class="info">ℹ️ Misskey라는 이름은 syuilo가 당시 듣고 있던 May'n의 노래, Brain Diver의 가사에서 유래했습니다.</div>
-誰でも開発に参加することができ、現在でも活発に開発が続いています。
+누구나 개발에 참가할 수 있으며, 현재도 활발한 개발이 이루어지고 있습니다.
-## 分散型とは何か?
+## 분산형이 뭐예요?
-<b>分散(distributed)型</b>とは、<b>非中央集権(decentralized)</b>とも呼ばれ、コミュニティが多数のサーバーに分散して存在し、それらが相互に<b>通信(連合、federation)</b>することでコンテンツ共有<b>ネットワーク(Fediverse)</b>を形成していることが特徴のサービスです。 単一のサーバーしか存在しない、もしくは複数存在しても互いに独立している場合は中央集権なサービスと言われ、例えばTwitterやFacebookなどほとんどのサービスがそれに該当します。 分散型のメリットは、自分に合った運営者やテーマのサーバーを選択できることです。自分でサーバーを作成することもできます。連合するおかげで、どのサーバーを選んでも、同じコミュニティにアクセスできます。
+<b>분산형</b>(distributed), 또는 <b>탈중앙화</b>(decentralized) 서비스의 특징은, 다양한 커뮤니티에 흩어져 있는 유저들이 서로 <b>교류(연합, federation)</b>하여 서로의 정보를 주고 받는 <b>네트워크(연합우주, Fediverse)</b>를 이루는 것입니다. 이에 대비되어 서버가 하나밖에 없거나, 여러 서버가 있더라도 독립되어 있는 경우는 중앙집권 서비스라고 하여, Twitter나 Facebook 등이 이에 속합니다. 분산형 서비스의 장점은 자신에게 맞는 운영자나 테마를 자유롭게 선택할 수 있는 것입니다. 직접 서버를 운영할 수도 있습니다. 다양한 서버가 연합한다는 특성으로, 어떤 서버를 고르더라도 같은 커뮤니티에 접근할 수 있습니다.
## 常にオープンソース
Misskeyはこれまでもこれからも、オープンソースであり続けます。オープンソースとは、簡単に言うと<b>ソフトウェアのソースコード(プログラム)が公開されている</b>ことです。ソースコードの修正や再配布が可能であることを定義に含めることもあります。 Misskeyのすべてのソースコードは[AGPL](https://github.com/misskey-dev/misskey/blob/develop/LICENSE)というオープンソースライセンスの下に[公開](https://github.com/misskey-dev)されていて、誰でも自由に閲覧、使用、修正、改変、再配布をすることができます。 オープンソースは、自分で好きなように変えたり、有害な処理が含まれていないことを確認することができたり、誰でも開発に参加できるなどの、様々なメリットがあります。 上述の分散型を実現するためにも、オープンソースであるということは必要不可欠な要素です。 再び引き合いに出しますが、TwitterやFacebookなどの利益を得ているほとんどのサービスはオープンソースではありません。
@@ -43,7 +43,7 @@ Misskeyはビジネスではなく、利用は無料であるため、収益は
## クレジット
Misskeyの開発者や、Misskeyに寄付をしてくださった方の一覧は[こちら](/about-misskey)で見ることができます。
-## よくある質問
+## 자주 묻는 질문
### プロジェクトは何を目指していますか?
強いて言うと、漠然的になりますが広く使われる汎用的なプラットフォームになることを目指しています。 Misskeyは他のプロジェクトとは違い、何らかの思想(例えば、反中央集権)やビジョンに基づいて開発が行われているわけではなく、その点ではフラットです。 それが逆に、特定の方向性に縛られないフレキシブルさを生み出すことに繋がっていると感じています。
<!-- TODO: ここにロードマップへのリンク -->
@@ -74,10 +74,10 @@ Misskeyは開発が進むにつれ使用する技術も大きく変わってき
### Mastodonのフォークですか?
いいえ。MisskeyはMastodonやその他のプロジェクトとは全く別のプロジェクトです。 開発に関しても、Misskeyの方が昔から開発されています。ただし、分散型になったのはMastodonの登場より後です。 同じActivityPubという分散のためのプロトコルを実装しているという点以外、両者に特に関りがあるわけでもありません。
-### iOS/Androidのアプリはありますか?
-公式にはそういったOSのネイティブアプリを開発していませんが、サードパーティ製のアプリがいくつかあります。 詳しくは[こちら](./apps)をご覧ください。
+### iOS/Android용 어플리케이션이 있나요?
+공식 어플리케이션은 없지만, 비공식 서드파티 어플리케이션을 사용하실 수 있습니다. 자세한 사항은 [여기를](./apps) 참조해 주세요.
-ただ、サードパーティ製アプリはどうしても機能への対応が遅れてしまうため、とくに拘りがなければ公式のWebクライアントの利用をおすすめします。 なお、MisskeyのWebクライアントはPWAに対応しているので、ネイティブアプリのように動作させることも可能です。 詳しくは[こちら](todo)をご覧ください。
+단, 비공식 어플리케이션의 경우 최신 기능을 사용할 수 있기까지 시간이 걸릴 수 있으므로, 대부분의 경우 공식 Web 클라이언트를 사용하시는 것을 추천드립니다. 또한, Misskey의 Web 클라이언트는 PWA를 지원하여, 브라우저에서 지원하는 경우 네이티브 앱처럼 사용할 수 있습니다. 자세한 사항은 [여기를](todo) 참조해 주세요.
### Misskeyのロゴ、アイコンはどこで入手できますか?
(準備中)
diff --git a/src/docs/zh-CN/advanced/create-plugin.md b/src/docs/zh-CN/advanced/create-plugin.md
index 5a9e6a1cfa..a32d36fa8e 100644
--- a/src/docs/zh-CN/advanced/create-plugin.md
+++ b/src/docs/zh-CN/advanced/create-plugin.md
@@ -2,7 +2,7 @@
Misskey Web客户端插件功能使您可以扩展客户端并添加各种功能。 我们在这里给出用于创建插件的元数据定义和AiScript API参考。
## 元数据
-プラグインは、AiScriptのメタデータ埋め込み機能を使って、デフォルトとしてプラグインのメタデータを定義する必要があります。 メタデータは次のプロパティを含むオブジェクトです。
+插件必须使用AiScript的元数据嵌入功能将插件的元数据定义为默认值。 元数据是一个包含以下属性的对象:
### name
插件名称
@@ -62,13 +62,13 @@ AiScript标准内置API将不会公布。
将项目添加到用户菜单。第一个参数是菜单项名字,第二个参数是该菜单项对应的回调函数。 目标用户对象作为第一个参数传给回调函数。
### Plugin:register_note_view_interruptor(fn)
-UIに表示されるノート情報を書き換えます。 コールバック関数には、第一引数に対象のノートオブジェクトが渡されます。 コールバック関数の返り値でノートが書き換えられます。
+改写显示在UI上的帖子信息。 目标帖子对象作为第一个参数传给回调函数。 该帖子将会使用回调函数的返回值进行改写。
### Plugin:register_note_post_interruptor(fn)
-ノート投稿時にノート情報を書き換えます。 コールバック関数には、第一引数に対象のノートオブジェクトが渡されます。 コールバック関数の返り値でノートが書き換えられます。
+发贴时改写帖子信息。 目标帖子对象作为第一个参数传给回调函数。 该帖子将会使用回调函数的返回值进行改写。
### Plugin:open_url(url)
-第一引数に渡されたURLをブラウザの新しいタブで開きます。
+在浏览器的新标签页中打开第一个参数传入的URL。
### Plugin:config
-プラグインの設定が格納されるオブジェクト。プラグイン定義のconfigで設定したキーで値が入ります。
+存储插件设置的对象。该值是通过插件定义的配置中设置的键值来传入的。
diff --git a/src/docs/zh-CN/advanced/develop-bot.md b/src/docs/zh-CN/advanced/develop-bot.md
index 7f825e9bc4..593c61891e 100644
--- a/src/docs/zh-CN/advanced/develop-bot.md
+++ b/src/docs/zh-CN/advanced/develop-bot.md
@@ -1,6 +1,6 @@
-# Botの作成
-[Misskey API](./api)を利用してBotの開発が可能です。 また、いくつかのBot実装が公開されているため、ぜひ参考にしてください。
+# 新建 Bot
+您可以使用 [Misskey API](./api)来开发Bot机器人。此外,一些机器人已经公开发布,您可以把它们作为参考。
-- [syuilo/ai](https://github.com/syuilo/ai) ... Node.js上で動く、TypeScript製Bot実装
+- [syuilo/ai](https://github.com/syuilo/ai) ... 运行在Node.js上的TypeScript实现的Bot
-Botを作成したときは、プロフィール設定からBotフラグをオンにしておくことを強くおすすめします。
+创建机器人时,我们强烈建议您在个人资料设置中启用Bot机器人标志。
diff --git a/src/docs/zh-CN/advanced/reversi-bot.md b/src/docs/zh-CN/advanced/reversi-bot.md
index 86637b775e..96a252b382 100644
--- a/src/docs/zh-CN/advanced/reversi-bot.md
+++ b/src/docs/zh-CN/advanced/reversi-bot.md
@@ -1,33 +1,33 @@
# Misskey黑白棋机器人开发
-Misskeyのリバーシ機能に対応したBotの開発方法をここに記します。
+下面列出的是为 Misskey 黑白棋功能开发一个Bot机器人的方法。
-1. `games/reversi`ストリームに以下のパラメータを付けて接続する:
- * `i`: botアカウントのAPIキー
+1. 使用以下参数来连接到`games/reversi`流:
+ * `i`: bot账号的API Key
-2. 対局への招待が来たら、ストリームから`invited`イベントが流れてくる
- * イベントの中身に、`parent`という名前で対局へ誘ってきたユーザーの情報が含まれている
+2. 当出现对局邀请时,流中会触发`invited`事件
+ * 事件内容中包含邀请您参加游戏的用户信息,用户名字为`parent`。
-3. `games/reversi/match`へ、`user_id`として`parent`の`id`が含まれたリクエストを送信する
+3. 向`games/reversi/match`发送请求,其中`user_id`包含`parent`的`id`
-4. 上手くいくとゲーム情報が返ってくるので、`games/reversi-game`ストリームへ、以下のパラメータを付けて接続する:
- * `i`: botアカウントのAPIキー
- * `game`: `game`の`id`
+4. 请求成功时将返回游戏信息,可以使用以下参数连接到`games/reversi-game`流:
+ * `i`: bot账号的API Key
+ * `game`: `game`的`id`
-5. この間、相手がゲームの設定を変更するとその都度`update-settings`イベントが流れてくるので、必要であれば何かしらの処理を行う
+5. 与此同时,每次对手更改游戏设置时,都会触发`update-settings`事件,如果有必要的话,需要对其进行处理。
-6. 設定に満足したら、`{ type: 'accept' }`メッセージをストリームに送信する
+6. 满足设定条件时,向流发送`{ type: 'accept' }`消息
-7. ゲームが開始すると、`started`イベントが流れてくる
- * イベントの中身にはゲーム情報が含まれている
+7. 游戏开始时会触发`started`事件
+ * 游戏状态信息会包含在该事件中
-8. 石を打つには、ストリームに`{ type: 'set', pos: <位置> }`を送信する(位置の計算方法は後述)
+8. 要放置棋子,向流发送`{ type: 'set', pos: <位置> }`(后面会说明位置的计算方法)
-9. 相手または自分が石を打つと、ストリームから`set`イベントが流れてくる
- * `color`として石の色が含まれている
- * `pos`として位置情報が含まれている
+9. 当对方或者您放置棋子时,会触发`set`事件
+ * `color`中包含该棋子的颜色
+ * `pos`中包含该棋子的位置
-## 位置の計算法
-8x8のマップを考える場合、各マスの位置(インデックスと呼びます)は次のようになっています:
+## 位置计算方法
+当棋盘尺寸为8x8时,每个方格的位置(称为索引)如下所示:
```
+--+--+--+--+--+--+--+--+
| 0| 1| 2| 3| 4| 5| 6| 7|
@@ -38,29 +38,29 @@ Misskeyのリバーシ機能に対応したBotの開発方法をここに記し
...
```
-### X,Y座標 から インデックス に変換する
+### 从X,Y坐标转换为索引
```
pos = x + (y * mapWidth)
```
-`mapWidth`は、ゲーム情報の`map`から、次のようにして計算できます:
+`mapWidth`可以根据游戏信息中的`map`,通过如下方法计算出来:
```
mapWidth = map[0].length
```
-### インデックス から X,Y座標 に変換する
+### 从索引转换为X,Y坐标
```
x = pos % mapWidth
y = Math.floor(pos / mapWidth)
```
-## マップ情報
-マップ情報は、ゲーム情報の`map`に入っています。 文字列の配列になっており、ひとつひとつの文字がマス情報を表しています。 それをもとにマップのデザインを知る事が出来ます:
-* `(スペース)` ... マス無し
-* `-` ... マス
-* `b` ... 初期配置される黒石
-* `w` ... 初期配置される白石
+## 棋盘信息
+棋盘信息包含在游戏信息的`map`中。 它是一个字符串数组,每个字符代表一块格子的信息。 您可以根据这些来了解地图如何设计:
+* `(空)` ... 没有格子
+* `-` ... 格子
+* `b` ... 黑子先下
+* `w` ... 白子先下
-例えば、4*4の次のような単純なマップがあるとします:
+以下面这个4*4的简单棋盘为例:
```text
+---+---+---+---+
| | | | |
@@ -73,23 +73,23 @@ y = Math.floor(pos / mapWidth)
+---+---+---+---+
```
-この場合、マップデータはこのようになります:
+这种情况下,棋盘数据是这样的:
```javascript
['----', '-wb-', '-bw-', '----']
```
-## ユーザーにフォームを提示して対話可能Botを作成する
-ユーザーとのコミュニケーションを行うため、ゲームの設定画面でユーザーにフォームを提示することができます。 例えば、Botの強さをユーザーが設定できるようにする、といったシナリオが考えられます。
+## 能和用户互动的交互式Bot机器人的创建
+要和用户交互,您可以在游戏设置屏幕上向用户显示提示窗口。 例如,可以让用户选择Bot机器人的难度。
-フォームを提示するには、`reversi-game`ストリームに次のメッセージを送信します:
+要显示窗口,需要向`reversi-game`流发送下列消息:
```javascript
{
type: 'init-form',
- body: [フォームコントロールの配列]
+ body: [表单控件数组]
}
```
-フォームコントロールの配列については今から説明します。 フォームコントロールは、次のようなオブジェクトです:
+下面说明窗口控件数组的结构。 窗口控件指的是如下面所示的对象:
```javascript
{
id: 'switch1',
@@ -98,10 +98,10 @@ y = Math.floor(pos / mapWidth)
value: false
}
```
-`id` ... コントロールのID。 `type` ... コントロールの種類。後述します。 `label` ... コントロールと一緒に表記するテキスト。 `value` ... コントロールのデフォルト値。
+`id` ... 控件ID。 `type` ... 控件类型。说明详见后文。 `label` ... 控件元素上显示的文字。 `value` ... 控件元素的默认值。
-### フォームの操作を受け取る
-ユーザーがフォームを操作すると、ストリームから`update-form`イベントが流れてきます。 イベントの中身には、コントロールのIDと、ユーザーが設定した値が含まれています。 例えば、上で示したスイッチをユーザーがオンにしたとすると、次のイベントが流れてきます:
+### 控件行为的处理
+当用户与对话框交互时将会触发流的`update-form`事件。 事件的内容包含控件的ID和用户设置的值。 例如,如果用户将上面显示的开关控件打开,则将触发以下事件:
```javascript
{
id: 'switch1',
@@ -109,18 +109,18 @@ y = Math.floor(pos / mapWidth)
}
```
-### フォームコントロールの種類
+### 窗口控件的类型
#### 开关
-type: `switch` スイッチを表示します。何かの機能をオン/オフさせたい場合に有用です。
+type: `switch` 显示一个开关。当您想要打开/关闭某些功能时非常有用。
-##### プロパティ
-`label` ... スイッチに表記するテキスト。
+##### 属性
+`label` ... 开关上显示的文字。
-#### ラジオボタン
-type: `radio` ラジオボタンを表示します。選択肢を提示するのに有用です。例えば、Botの強さを設定させるなどです。
+#### 单选按钮
+type: `radio` 显示一个单选按钮。用来表示单项选择。例如,可以选择Bot机器人的难度。
-##### プロパティ
-`items` ... ラジオボタンの選択肢。例:
+##### 属性
+`items` ... 单元按钮的选择项。例:
```javascript
items: [{
label: '弱',
@@ -129,32 +129,32 @@ items: [{
label: '中',
value: 2
}, {
- label: '強',
+ label: '强',
value: 3
}]
```
-#### スライダー
-type: `slider` スライダーを表示します。
+#### 滑块
+type: `slider` 显示一个滑块。
-##### プロパティ
-`min` ... スライダーの下限。 `max` ... スライダーの上限。 `step` ... 入力欄で刻むステップ値。
+##### 属性
+`min` ... 滑块最小值。 `max` ... 滑块最大值。 `step` ... 滑块值的步长。
-#### テキストボックス
-type: `textbox` テキストボックスを表示します。ユーザーになにか入力させる一般的な用途に利用できます。
+#### 文本框
+type: `textbox` 显示一个文本框。可以在各种需要用户输入的地方使用。
-## ユーザーにメッセージを表示する
-設定画面でユーザーと対話する、フォーム以外のもうひとつの方法がこれです。ユーザーになにかメッセージを表示することができます。 例えば、ユーザーがBotの対応していないモードやマップを選択したとき、警告を表示するなどです。 メッセージを表示するには、次のメッセージをストリームに送信します:
+## 向用户显示消息
+设置屏幕上与用户交互,是除了对话框外的另一种方法。您可以向用户显示一条消息。 例如,当用户选择Bot机器人不支持的模式或棋盘时显示警告。 要显示消息,请将以下消息发送到流:
```javascript
{
type: 'message',
body: {
- text: 'メッセージ内容',
- type: 'メッセージの種類'
+ text: '消息内容',
+ type: '消息类型'
}
}
```
-メッセージの種類: `success`, `info`, `warning`, `error`。
+消息类型:`success`, `info`, `warning`, `error`。
-## 投了する
-投了をするには、<a href="./api/endpoints/games/reversi/games/surrender">このエンドポイント</a>にリクエストします。
+## 认输
+要认输,请发送请求到<a href="./api/endpoints/games/reversi/games/surrender">这个终端</a>。
diff --git a/src/docs/zh-CN/advanced/stream.md b/src/docs/zh-CN/advanced/stream.md
index b4cd7ec104..090f8475ea 100644
--- a/src/docs/zh-CN/advanced/stream.md
+++ b/src/docs/zh-CN/advanced/stream.md
@@ -1,23 +1,23 @@
# 流式API
-ストリーミングAPIを使うと、リアルタイムで様々な情報(例えばタイムラインに新しい投稿が流れてきた、メッセージが届いた、フォローされた、など)を受け取ったり、様々な操作を行ったりすることができます。
+通过流式API,您可以实时接收各种信息(例如,你的时间线中的新帖文,收到的消息,关注等),并进行各种操作。
-## ストリームに接続する
+## 连接到流
-ストリーミングAPIを利用するには、まずMisskeyサーバーに**websocket**接続する必要があります。
+要使用流式API,您需要使用**websocket**连接到Misskey服务器。
-以下のURLに、`i`というパラメータ名で認証情報を含めて、websocket接続してください。例:
+请使用参数`i`连接到以下URL,并在websocket连接中包含认证信息。例如:
```
%WS_URL%/streaming?i=xxxxxxxxxxxxxxx
```
-認証情報は、自分のAPIキーや、アプリケーションからストリームに接続する際はユーザーのアクセストークンのことを指します。
+认证信息是您的API密钥,从应用程序连接到流时需要引用的用户访问令牌
-<div class="info">ℹ️ 認証情報の取得については、<a href="./api">こちらのドキュメント</a>をご確認ください。</div>
+<div class="info">ℹ️ 关于如何获取认证信息,请参考<a href="./api">此文档</a>。</div>
---
-認証情報は省略することもできますが、その場合非ログインでの利用ということになり、受信できる情報や可能な操作は限られます。例:
+您可以省略身份验证信息。此时无需登录即可使用,但是可以接收的信息和可以执行的操作将受到限制。例:
```
%WS_URL%/streaming
@@ -215,7 +215,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
* `reaction`に、リアクションの種類が設定されます。
* `userId`に、リアクションを行ったユーザーのIDが設定されます。
-例:
+例:
```json
{
type: 'noteUpdated',
@@ -235,7 +235,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
* `deletedAt`に、削除日時が設定されます。
-例:
+例:
```json
{
type: 'noteUpdated',
@@ -255,7 +255,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
* `choice`に、選択肢IDが設定されます。
* `userId`に、投票を行ったユーザーのIDが設定されます。
-例:
+例:
```json
{
type: 'noteUpdated',
diff --git a/src/docs/zh-CN/features/timeline.md b/src/docs/zh-CN/features/timeline.md
index 8ecda37ada..6aa4e8b358 100644
--- a/src/docs/zh-CN/features/timeline.md
+++ b/src/docs/zh-CN/features/timeline.md
@@ -14,18 +14,18 @@
全てのローカルユーザーの「ホーム」指定されていない投稿と、サーバーに届いた全てのリモートユーザーの「ホーム」指定されていない投稿が流れます。GTLと略されます。
## 比较
-| ソース | | | 时间线 | | |
-| ------------ | --- | -- | --- | -- | -- |
-| 用户 | 可见性 | 首页 | 本地 | 社交 | 全局 |
-| ローカル (フォロー) | 发布 | ✔ | ✔ | ✔ | ✔ |
-| | 首页 | ✔ | | ✔ | |
-| | 关注者 | ✔ | ✔ | ✔ | ✔ |
-| リモート (フォロー) | 发布 | ✔ | | ✔ | ✔ |
-| | 首页 | ✔ | | ✔ | |
-| | 关注者 | ✔ | | ✔ | ✔ |
-| ローカル (未フォロー) | 发布 | | ✔ | ✔ | ✔ |
-| | 首页 | | | | |
-| | 关注者 | | | | |
-| リモート (未フォロー) | 发布 | | | | ✔ |
-| | 首页 | | | | |
-| | 关注者 | | | | |
+| 来源 | | | 时间线 | | |
+| ------- | --- | -- | --- | -- | -- |
+| 用户 | 可见性 | 首页 | 本地 | 社交 | 全局 |
+| 本地(关注) | 发布 | ✔ | ✔ | ✔ | ✔ |
+| | 首页 | ✔ | | ✔ | |
+| | 关注者 | ✔ | ✔ | ✔ | ✔ |
+| 跨站(关注) | 发布 | ✔ | | ✔ | ✔ |
+| | 首页 | ✔ | | ✔ | |
+| | 关注者 | ✔ | | ✔ | ✔ |
+| 本地(未关注) | 发布 | | ✔ | ✔ | ✔ |
+| | 首页 | | | | |
+| | 关注者 | | | | |
+| 跨站(未关注) | 发布 | | | | ✔ |
+| | 首页 | | | | |
+| | 关注者 | | | | |
diff --git a/src/docs/zh-CN/general/apps.md b/src/docs/zh-CN/general/apps.md
index 1f4c85fe8f..3bd88df19b 100644
--- a/src/docs/zh-CN/general/apps.md
+++ b/src/docs/zh-CN/general/apps.md
@@ -1,6 +1,6 @@
-# サードパーティアプリのリスト
-## クライアント
+# 第三方应用程序列表
+## 客户端
todo
-## 連携サービス
+## 链接服务
todo
diff --git a/src/docs/zh-CN/general/changelog.md b/src/docs/zh-CN/general/changelog.md
index 6766a63b20..53dfae95c3 100644
--- a/src/docs/zh-CN/general/changelog.md
+++ b/src/docs/zh-CN/general/changelog.md
@@ -1,5 +1,5 @@
-# 更新履歴
-<div class="info">ℹ️ このサーバーの更新履歴です。Misskeyの最新のリリースについては、<a href="https://github.com/misskey-dev/misskey/blob/master/CHANGELOG.md" target="_blank">GitHub</a>をご確認ください。</div>
+# 更新日志
+<div class="info">ℹ️ 这是该服务器的更新日志。请在 <a href="https://github.com/misskey-dev/misskey/blob/master/CHANGELOG.md" target="_blank">GitHub</a> 上检查 Misskey 的最新发布版本。</div>
<!-- For translators: Do not edit these comments. -->
<!--[CHANGELOG]-->
diff --git a/src/docs/zh-CN/general/links.md b/src/docs/zh-CN/general/links.md
index 3171cac1f8..d234511768 100644
--- a/src/docs/zh-CN/general/links.md
+++ b/src/docs/zh-CN/general/links.md
@@ -1,12 +1,12 @@
-# リンク集
+# 相关链接
-## Webサイト
-- [Official Discord](https://discord.gg/Wp8gVStHW3) - Misskey公式Discordサーバー
-- [Misskey Forum](https://forum.misskey.io/) - Misskeyに関する話題を扱うフォーラム
+## 网站
+- [Official Discord](https://discord.gg/Wp8gVStHW3) - Misskey官方Discord服务器
+- [Misskey Forum](https://forum.misskey.io/) - Misskey相关主题的论坛
## 账户
- [@repo@misskey.io](https://misskey.io/@repo) - 发布Misskey的存储库更新的机器人
-## ライブラリ
-- [misskey-dev/misskey.js](https://github.com/misskey-dev/misskey.js) - JavaScriptのMisskey SDK
-- [misskey-dev/mfm.js](https://github.com/misskey-dev/mfm.js) - JavaScriptのMFMパーサー実装
+## 库
+- [misskey-dev/misskey.js](https://github.com/misskey-dev/misskey.js) - JavaScript的Misskey SDK
+- [misskey-dev/mfm.js](https://github.com/misskey-dev/mfm.js) - JavaScript的MFM解析器实现
diff --git a/src/docs/zh-CN/general/report-issue.md b/src/docs/zh-CN/general/report-issue.md
index 63527e32af..22d97ff3d5 100644
--- a/src/docs/zh-CN/general/report-issue.md
+++ b/src/docs/zh-CN/general/report-issue.md
@@ -1,4 +1,4 @@
-# 不具合の報告
+# bug报告
不具合と思われる状況に遭遇したときは、まず[トラブルシューティング](./troubleshooting)をご一読ください。 それでも問題が解決しないときは、以下の情報を含めて[フォーラム](https://forum.misskey.io/)に投稿してください。 投稿することで、解決策が見つかったり、不具合と判断されれば開発チームによって修正が行われます。
## 含める情報
diff --git a/src/misc/api-permissions.ts b/src/misc/api-permissions.ts
index eb20c3d289..160cdf9fd6 100644
--- a/src/misc/api-permissions.ts
+++ b/src/misc/api-permissions.ts
@@ -32,3 +32,4 @@ export const kinds = [
'read:gallery-likes',
'write:gallery-likes',
];
+// IF YOU ADD KINDS(PERMISSIONS), YOU MUST ADD TRANSLATIONS (under _permissions).
diff --git a/src/misc/check-hit-antenna.ts b/src/misc/check-hit-antenna.ts
index 38965f4b0d..3789054b26 100644
--- a/src/misc/check-hit-antenna.ts
+++ b/src/misc/check-hit-antenna.ts
@@ -3,13 +3,13 @@ import { Note } from '@/models/entities/note';
import { User } from '@/models/entities/user';
import { UserListJoinings, UserGroupJoinings } from '@/models/index';
import { getFullApAccount } from './convert-host';
-import { PackedNote } from '../models/repositories/note';
import { parseAcct } from '@/misc/acct';
+import { Packed } from './schema';
/**
* noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい
*/
-export async function checkHitAntenna(antenna: Antenna, note: (Note | PackedNote), noteUser: { username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> {
+export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> {
if (note.visibility === 'specified') return false;
if (note.visibility === 'followers') {
diff --git a/src/misc/schema.ts b/src/misc/schema.ts
index e14e6e0dd7..4131875ef7 100644
--- a/src/misc/schema.ts
+++ b/src/misc/schema.ts
@@ -1,15 +1,65 @@
-export type Schema = {
- type: 'boolean' | 'number' | 'string' | 'array' | 'object' | 'any';
- nullable: boolean;
- optional: boolean;
+import { SimpleObj, SimpleSchema } from './simple-schema';
+import { packedUserSchema } from '@/models/repositories/user';
+import { packedNoteSchema } from '@/models/repositories/note';
+import { packedUserListSchema } from '@/models/repositories/user-list';
+import { packedAppSchema } from '@/models/repositories/app';
+import { packedMessagingMessageSchema } from '@/models/repositories/messaging-message';
+import { packedNotificationSchema } from '@/models/repositories/notification';
+import { packedDriveFileSchema } from '@/models/repositories/drive-file';
+import { packedDriveFolderSchema } from '@/models/repositories/drive-folder';
+import { packedFollowingSchema } from '@/models/repositories/following';
+import { packedMutingSchema } from '@/models/repositories/muting';
+import { packedBlockingSchema } from '@/models/repositories/blocking';
+import { packedNoteReactionSchema } from '@/models/repositories/note-reaction';
+import { packedHashtagSchema } from '@/models/repositories/hashtag';
+import { packedPageSchema } from '@/models/repositories/page';
+import { packedUserGroupSchema } from '@/models/repositories/user-group';
+import { packedNoteFavoriteSchema } from '@/models/repositories/note-favorite';
+import { packedChannelSchema } from '@/models/repositories/channel';
+import { packedAntennaSchema } from '@/models/repositories/antenna';
+import { packedClipSchema } from '@/models/repositories/clip';
+import { packedFederationInstanceSchema } from '@/models/repositories/federation-instance';
+import { packedQueueCountSchema } from '@/models/repositories/queue';
+import { packedGalleryPostSchema } from '@/models/repositories/gallery-post';
+import { packedEmojiSchema } from '@/models/repositories/emoji';
+import { packedReversiGameSchema } from '@/models/repositories/games/reversi/game';
+import { packedReversiMatchingSchema } from '@/models/repositories/games/reversi/matching';
+
+export const refs = {
+ User: packedUserSchema,
+ UserList: packedUserListSchema,
+ UserGroup: packedUserGroupSchema,
+ App: packedAppSchema,
+ MessagingMessage: packedMessagingMessageSchema,
+ Note: packedNoteSchema,
+ NoteReaction: packedNoteReactionSchema,
+ NoteFavorite: packedNoteFavoriteSchema,
+ Notification: packedNotificationSchema,
+ DriveFile: packedDriveFileSchema,
+ DriveFolder: packedDriveFolderSchema,
+ Following: packedFollowingSchema,
+ Muting: packedMutingSchema,
+ Blocking: packedBlockingSchema,
+ Hashtag: packedHashtagSchema,
+ Page: packedPageSchema,
+ Channel: packedChannelSchema,
+ QueueCount: packedQueueCountSchema,
+ Antenna: packedAntennaSchema,
+ Clip: packedClipSchema,
+ FederationInstance: packedFederationInstanceSchema,
+ GalleryPost: packedGalleryPostSchema,
+ Emoji: packedEmojiSchema,
+ ReversiGame: packedReversiGameSchema,
+ ReversiMatching: packedReversiMatchingSchema,
+};
+
+export type Packed<x extends keyof typeof refs> = ObjType<(typeof refs[x])['properties']>;
+
+export interface Schema extends SimpleSchema {
items?: Schema;
properties?: Obj;
- description?: string;
- example?: any;
- format?: string;
- ref?: string;
- enum?: string[];
-};
+ ref?: keyof typeof refs;
+}
type NonUndefinedPropertyNames<T extends Obj> = {
[K in keyof T]: T[K]['optional'] extends true ? never : K
@@ -22,7 +72,7 @@ type UndefinedPropertyNames<T extends Obj> = {
type OnlyRequired<T extends Obj> = Pick<T, NonUndefinedPropertyNames<T>>;
type OnlyOptional<T extends Obj> = Pick<T, UndefinedPropertyNames<T>>;
-export type Obj = { [key: string]: Schema };
+export interface Obj extends SimpleObj { [key: string]: Schema; }
export type ObjType<s extends Obj> =
{ [P in keyof OnlyOptional<s>]?: SchemaType<s[P]> } &
@@ -48,6 +98,10 @@ export type SchemaType<p extends Schema> =
p['type'] extends 'string' ? NullOrUndefined<p, string> :
p['type'] extends 'boolean' ? NullOrUndefined<p, boolean> :
p['type'] extends 'array' ? NullOrUndefined<p, MyType<NonNullable<p['items']>>[]> :
- p['type'] extends 'object' ? NullOrUndefined<p, ObjType<NonNullable<p['properties']>>> :
+ p['type'] extends 'object' ? (
+ p['ref'] extends keyof typeof refs
+ ? NullOrUndefined<p, Packed<p['ref']>>
+ : NullOrUndefined<p, ObjType<NonNullable<p['properties']>>>
+ ) :
p['type'] extends 'any' ? NullOrUndefined<p, any> :
any;
diff --git a/src/misc/simple-schema.ts b/src/misc/simple-schema.ts
new file mode 100644
index 0000000000..abbb348e24
--- /dev/null
+++ b/src/misc/simple-schema.ts
@@ -0,0 +1,15 @@
+export interface SimpleSchema {
+ type: 'boolean' | 'number' | 'string' | 'array' | 'object' | 'any';
+ nullable: boolean;
+ optional: boolean;
+ items?: SimpleSchema;
+ properties?: SimpleObj;
+ description?: string;
+ example?: any;
+ format?: string;
+ ref?: string;
+ enum?: string[];
+ default?: boolean | null;
+}
+
+export interface SimpleObj { [key: string]: SimpleSchema; }
diff --git a/src/models/repositories/antenna.ts b/src/models/repositories/antenna.ts
index e61eed5e08..657de55581 100644
--- a/src/models/repositories/antenna.ts
+++ b/src/models/repositories/antenna.ts
@@ -1,15 +1,13 @@
import { EntityRepository, Repository } from 'typeorm';
import { Antenna } from '@/models/entities/antenna';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
import { AntennaNotes, UserGroupJoinings } from '../index';
-export type PackedAntenna = SchemaType<typeof packedAntennaSchema>;
-
@EntityRepository(Antenna)
export class AntennaRepository extends Repository<Antenna> {
public async pack(
src: Antenna['id'] | Antenna,
- ): Promise<PackedAntenna> {
+ ): Promise<Packed<'Antenna'>> {
const antenna = typeof src === 'object' ? src : await this.findOneOrFail(src);
const hasUnreadNote = (await AntennaNotes.findOne({ antennaId: antenna.id, read: false })) != null;
diff --git a/src/models/repositories/app.ts b/src/models/repositories/app.ts
index 2287bd4390..0226edad11 100644
--- a/src/models/repositories/app.ts
+++ b/src/models/repositories/app.ts
@@ -1,9 +1,8 @@
import { EntityRepository, Repository } from 'typeorm';
import { App } from '@/models/entities/app';
import { AccessTokens } from '../index';
-import { SchemaType } from '@/misc/schema';
-
-export type PackedApp = SchemaType<typeof packedAppSchema>;
+import { Packed } from '@/misc/schema';
+import { User } from '../entities/user';
@EntityRepository(App)
export class AppRepository extends Repository<App> {
@@ -15,7 +14,7 @@ export class AppRepository extends Repository<App> {
includeSecret?: boolean,
includeProfileImageIds?: boolean
}
- ): Promise<PackedApp> {
+ ): Promise<Packed<'App'>> {
const opts = Object.assign({
detail: false,
includeSecret: false,
@@ -52,13 +51,9 @@ export const packedAppSchema = {
type: 'string' as const,
optional: false as const, nullable: false as const
},
- createdAt: {
+ callbackUrl: {
type: 'string' as const,
- optional: false as const, nullable: false as const
- },
- lastUsedAt: {
- type: 'string' as const,
- optional: false as const, nullable: false as const
+ optional: false as const, nullable: true as const
},
permission: {
type: 'array' as const,
diff --git a/src/models/repositories/blocking.ts b/src/models/repositories/blocking.ts
index dd3a10905c..ac60c9a4ce 100644
--- a/src/models/repositories/blocking.ts
+++ b/src/models/repositories/blocking.ts
@@ -2,17 +2,15 @@ import { EntityRepository, Repository } from 'typeorm';
import { Users } from '../index';
import { Blocking } from '@/models/entities/blocking';
import { awaitAll } from '@/prelude/await-all';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
import { User } from '@/models/entities/user';
-export type PackedBlocking = SchemaType<typeof packedBlockingSchema>;
-
@EntityRepository(Blocking)
export class BlockingRepository extends Repository<Blocking> {
public async pack(
src: Blocking['id'] | Blocking,
me?: { id: User['id'] } | null | undefined
- ): Promise<PackedBlocking> {
+ ): Promise<Packed<'Blocking'>> {
const blocking = typeof src === 'object' ? src : await this.findOneOrFail(src);
return await awaitAll({
@@ -56,7 +54,7 @@ export const packedBlockingSchema = {
blockee: {
type: 'object' as const,
optional: false as const, nullable: false as const,
- ref: 'User',
+ ref: 'User' as const,
},
}
};
diff --git a/src/models/repositories/channel.ts b/src/models/repositories/channel.ts
index 4bb829f570..5c7d095473 100644
--- a/src/models/repositories/channel.ts
+++ b/src/models/repositories/channel.ts
@@ -1,17 +1,15 @@
import { EntityRepository, Repository } from 'typeorm';
import { Channel } from '@/models/entities/channel';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
import { DriveFiles, ChannelFollowings, NoteUnreads } from '../index';
import { User } from '@/models/entities/user';
-export type PackedChannel = SchemaType<typeof packedChannelSchema>;
-
@EntityRepository(Channel)
export class ChannelRepository extends Repository<Channel> {
public async pack(
src: Channel['id'] | Channel,
me?: { id: User['id'] } | null | undefined,
- ): Promise<PackedChannel> {
+ ): Promise<Packed<'Channel'>> {
const channel = typeof src === 'object' ? src : await this.findOneOrFail(src);
const meId = me ? me.id : null;
diff --git a/src/models/repositories/clip.ts b/src/models/repositories/clip.ts
index 49dc3a332e..7892811d48 100644
--- a/src/models/repositories/clip.ts
+++ b/src/models/repositories/clip.ts
@@ -1,16 +1,14 @@
import { EntityRepository, Repository } from 'typeorm';
import { Clip } from '@/models/entities/clip';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
import { Users } from '../index';
import { awaitAll } from '@/prelude/await-all';
-export type PackedClip = SchemaType<typeof packedClipSchema>;
-
@EntityRepository(Clip)
export class ClipRepository extends Repository<Clip> {
public async pack(
src: Clip['id'] | Clip,
- ): Promise<PackedClip> {
+ ): Promise<Packed<'Clip'>> {
const clip = typeof src === 'object' ? src : await this.findOneOrFail(src);
return await awaitAll({
@@ -53,7 +51,7 @@ export const packedClipSchema = {
},
user: {
type: 'object' as const,
- ref: 'User',
+ ref: 'User' as const,
optional: false as const, nullable: false as const,
},
name: {
diff --git a/src/models/repositories/drive-file.ts b/src/models/repositories/drive-file.ts
index 42a60ff03c..ddf9a46afd 100644
--- a/src/models/repositories/drive-file.ts
+++ b/src/models/repositories/drive-file.ts
@@ -4,14 +4,12 @@ import { Users, DriveFolders } from '../index';
import { User } from '@/models/entities/user';
import { toPuny } from '@/misc/convert-host';
import { awaitAll } from '@/prelude/await-all';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
import config from '@/config/index';
import { query, appendQuery } from '@/prelude/url';
import { Meta } from '@/models/entities/meta';
import { fetchMeta } from '@/misc/fetch-meta';
-export type PackedDriveFile = SchemaType<typeof packedDriveFileSchema>;
-
type PackOptions = {
detail?: boolean,
self?: boolean,
@@ -99,12 +97,12 @@ export class DriveFileRepository extends Repository<DriveFile> {
return parseInt(sum, 10) || 0;
}
- public async pack(src: DriveFile['id'], options?: PackOptions): Promise<PackedDriveFile | null>;
- public async pack(src: DriveFile, options?: PackOptions): Promise<PackedDriveFile>;
+ public async pack(src: DriveFile['id'], options?: PackOptions): Promise<Packed<'DriveFile'> | null>;
+ public async pack(src: DriveFile, options?: PackOptions): Promise<Packed<'DriveFile'>>;
public async pack(
src: DriveFile['id'] | DriveFile,
options?: PackOptions
- ): Promise<PackedDriveFile | null> {
+ ): Promise<Packed<'DriveFile'> | null> {
const opts = Object.assign({
detail: false,
self: false
@@ -234,7 +232,7 @@ export const packedDriveFileSchema = {
folder: {
type: 'object' as const,
optional: true as const, nullable: true as const,
- ref: 'DriveFolder'
+ ref: 'DriveFolder' as const,
},
userId: {
type: 'string' as const,
@@ -245,7 +243,7 @@ export const packedDriveFileSchema = {
user: {
type: 'object' as const,
optional: true as const, nullable: true as const,
- ref: 'User'
+ ref: 'User' as const,
}
},
};
diff --git a/src/models/repositories/drive-folder.ts b/src/models/repositories/drive-folder.ts
index 4228284f82..8ef6f01b5d 100644
--- a/src/models/repositories/drive-folder.ts
+++ b/src/models/repositories/drive-folder.ts
@@ -2,9 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import { DriveFolders, DriveFiles } from '../index';
import { DriveFolder } from '@/models/entities/drive-folder';
import { awaitAll } from '@/prelude/await-all';
-import { SchemaType } from '@/misc/schema';
-
-export type PackedDriveFolder = SchemaType<typeof packedDriveFolderSchema>;
+import { Packed } from '@/misc/schema';
@EntityRepository(DriveFolder)
export class DriveFolderRepository extends Repository<DriveFolder> {
@@ -20,7 +18,7 @@ export class DriveFolderRepository extends Repository<DriveFolder> {
options?: {
detail: boolean
}
- ): Promise<PackedDriveFolder> {
+ ): Promise<Packed<'DriveFolder'>> {
const opts = Object.assign({
detail: false
}, options);
@@ -87,7 +85,7 @@ export const packedDriveFolderSchema = {
parent: {
type: 'object' as const,
optional: true as const, nullable: true as const,
- ref: 'DriveFolder'
+ ref: 'DriveFolder' as const,
},
},
};
diff --git a/src/models/repositories/emoji.ts b/src/models/repositories/emoji.ts
index c3d7184ec9..7985c27aba 100644
--- a/src/models/repositories/emoji.ts
+++ b/src/models/repositories/emoji.ts
@@ -1,11 +1,12 @@
import { EntityRepository, Repository } from 'typeorm';
import { Emoji } from '@/models/entities/emoji';
+import { Packed } from '@/misc/schema';
@EntityRepository(Emoji)
export class EmojiRepository extends Repository<Emoji> {
public async pack(
src: Emoji['id'] | Emoji,
- ) {
+ ): Promise<Packed<'Emoji'>> {
const emoji = typeof src === 'object' ? src : await this.findOneOrFail(src);
return {
@@ -24,3 +25,41 @@ export class EmojiRepository extends Repository<Emoji> {
return Promise.all(emojis.map(x => this.pack(x)));
}
}
+
+export const packedEmojiSchema = {
+ type: 'object' as const,
+ optional: false as const, nullable: false as const,
+ properties: {
+ id: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ format: 'id',
+ example: 'xxxxxxxxxx',
+ },
+ aliases: {
+ type: 'array' as const,
+ optional: false as const, nullable: false as const,
+ items: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ format: 'id',
+ },
+ },
+ name: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ },
+ category: {
+ type: 'string' as const,
+ optional: false as const, nullable: true as const,
+ },
+ host: {
+ type: 'string' as const,
+ optional: false as const, nullable: true as const,
+ },
+ url: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ },
+ }
+};
diff --git a/src/models/repositories/following.ts b/src/models/repositories/following.ts
index 3bb120bc4b..b1f716069f 100644
--- a/src/models/repositories/following.ts
+++ b/src/models/repositories/following.ts
@@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import { Users } from '../index';
import { Following } from '@/models/entities/following';
import { awaitAll } from '@/prelude/await-all';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
import { User } from '@/models/entities/user';
type LocalFollowerFollowing = Following & {
@@ -29,8 +29,6 @@ type RemoteFolloweeFollowing = Following & {
followeeSharedInbox: string;
};
-export type PackedFollowing = SchemaType<typeof packedFollowingSchema>;
-
@EntityRepository(Following)
export class FollowingRepository extends Repository<Following> {
public isLocalFollower(following: Following): following is LocalFollowerFollowing {
@@ -56,7 +54,7 @@ export class FollowingRepository extends Repository<Following> {
populateFollowee?: boolean;
populateFollower?: boolean;
}
- ): Promise<PackedFollowing> {
+ ): Promise<Packed<'Following'>> {
const following = typeof src === 'object' ? src : await this.findOneOrFail(src);
if (opts == null) opts = {};
@@ -110,7 +108,7 @@ export const packedFollowingSchema = {
followee: {
type: 'object' as const,
optional: true as const, nullable: false as const,
- ref: 'User',
+ ref: 'User' as const,
},
followerId: {
type: 'string' as const,
@@ -120,7 +118,7 @@ export const packedFollowingSchema = {
follower: {
type: 'object' as const,
optional: true as const, nullable: false as const,
- ref: 'User',
+ ref: 'User' as const,
},
}
};
diff --git a/src/models/repositories/gallery-post.ts b/src/models/repositories/gallery-post.ts
index 03edb35213..4f666ff252 100644
--- a/src/models/repositories/gallery-post.ts
+++ b/src/models/repositories/gallery-post.ts
@@ -1,18 +1,16 @@
import { EntityRepository, Repository } from 'typeorm';
import { GalleryPost } from '@/models/entities/gallery-post';
-import { SchemaType } from '../../misc/schema';
+import { Packed } from '@/misc/schema';
import { Users, DriveFiles, GalleryLikes } from '../index';
import { awaitAll } from '@/prelude/await-all';
import { User } from '@/models/entities/user';
-export type PackedGalleryPost = SchemaType<typeof packedGalleryPostSchema>;
-
@EntityRepository(GalleryPost)
export class GalleryPostRepository extends Repository<GalleryPost> {
public async pack(
src: GalleryPost['id'] | GalleryPost,
me?: { id: User['id'] } | null | undefined,
- ): Promise<PackedGalleryPost> {
+ ): Promise<Packed<'GalleryPost'>> {
const meId = me ? me.id : null;
const post = typeof src === 'object' ? src : await this.findOneOrFail(src);
@@ -76,7 +74,7 @@ export const packedGalleryPostSchema = {
},
user: {
type: 'object' as const,
- ref: 'User',
+ ref: 'User' as const,
optional: false as const, nullable: false as const,
},
fileIds: {
@@ -94,7 +92,7 @@ export const packedGalleryPostSchema = {
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
- ref: 'DriveFile'
+ ref: 'DriveFile' as const,
}
},
tags: {
diff --git a/src/models/repositories/games/reversi/game.ts b/src/models/repositories/games/reversi/game.ts
index dc91ad51b8..9adb386fa9 100644
--- a/src/models/repositories/games/reversi/game.ts
+++ b/src/models/repositories/games/reversi/game.ts
@@ -2,6 +2,7 @@ import { User } from '@/models/entities/user';
import { EntityRepository, Repository } from 'typeorm';
import { Users } from '../../../index';
import { ReversiGame } from '@/models/entities/games/reversi/game';
+import { Packed } from '@/misc/schema';
@EntityRepository(ReversiGame)
export class ReversiGameRepository extends Repository<ReversiGame> {
@@ -11,7 +12,7 @@ export class ReversiGameRepository extends Repository<ReversiGame> {
options?: {
detail?: boolean
}
- ) {
+ ): Promise<Packed<'ReversiGame'>> {
const opts = Object.assign({
detail: true
}, options);
@@ -20,8 +21,8 @@ export class ReversiGameRepository extends Repository<ReversiGame> {
return {
id: game.id,
- createdAt: game.createdAt,
- startedAt: game.startedAt,
+ createdAt: game.createdAt.toISOString(),
+ startedAt: game.startedAt && game.startedAt.toISOString(),
isStarted: game.isStarted,
isEnded: game.isEnded,
form1: game.form1,
@@ -41,9 +42,150 @@ export class ReversiGameRepository extends Repository<ReversiGame> {
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
...(opts.detail ? {
- logs: game.logs,
+ logs: game.logs.map(log => ({
+ at: log.at.toISOString(),
+ color: log.color,
+ pos: log.pos
+ })),
map: game.map,
} : {})
};
}
}
+
+export const packedReversiGameSchema = {
+ type: 'object' as const,
+ optional: false as const, nullable: false as const,
+ properties: {
+ id: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ format: 'id',
+ example: 'xxxxxxxxxx',
+ },
+ createdAt: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ format: 'date-time',
+ },
+ startedAt: {
+ type: 'string' as const,
+ optional: false as const, nullable: true as const,
+ format: 'date-time',
+ },
+ isStarted: {
+ type: 'boolean' as const,
+ optional: false as const, nullable: false as const,
+ },
+ isEnded: {
+ type: 'boolean' as const,
+ optional: false as const, nullable: false as const,
+ },
+ form1: {
+ type: 'any' as const,
+ optional: false as const, nullable: true as const,
+ },
+ form2: {
+ type: 'any' as const,
+ optional: false as const, nullable: true as const,
+ },
+ user1Accepted: {
+ type: 'boolean' as const,
+ optional: false as const, nullable: false as const,
+ },
+ user2Accepted: {
+ type: 'boolean' as const,
+ optional: false as const, nullable: false as const,
+ },
+ user1Id: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ format: 'id',
+ example: 'xxxxxxxxxx',
+ },
+ user2Id: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ format: 'id',
+ example: 'xxxxxxxxxx',
+ },
+ user1: {
+ type: 'object' as const,
+ optional: false as const, nullable: false as const,
+ ref: 'User' as const,
+ },
+ user2: {
+ type: 'object' as const,
+ optional: false as const, nullable: false as const,
+ ref: 'User' as const,
+ },
+ winnerId: {
+ type: 'string' as const,
+ optional: false as const, nullable: true as const,
+ format: 'id',
+ example: 'xxxxxxxxxx',
+ },
+ winner: {
+ type: 'object' as const,
+ optional: false as const, nullable: true as const,
+ ref: 'User' as const,
+ },
+ surrendered: {
+ type: 'string' as const,
+ optional: false as const, nullable: true as const,
+ format: 'id',
+ example: 'xxxxxxxxxx',
+ },
+ black: {
+ type: 'number' as const,
+ optional: false as const, nullable: true as const,
+ },
+ bw: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ },
+ isLlotheo: {
+ type: 'boolean' as const,
+ optional: false as const, nullable: false as const,
+ },
+ canPutEverywhere: {
+ type: 'boolean' as const,
+ optional: false as const, nullable: false as const,
+ },
+ loopedBoard: {
+ type: 'boolean' as const,
+ optional: false as const, nullable: false as const,
+ },
+ logs: {
+ type: 'array' as const,
+ optional: true as const, nullable: false as const,
+ items: {
+ type: 'object' as const,
+ optional: true as const, nullable: false as const,
+ properties: {
+ at: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ format: 'date-time',
+ },
+ color: {
+ type: 'boolean' as const,
+ optional: false as const, nullable: false as const,
+ },
+ pos: {
+ type: 'number' as const,
+ optional: false as const, nullable: false as const,
+ },
+ }
+ }
+ },
+ map: {
+ type: 'array' as const,
+ optional: true as const, nullable: false as const,
+ items: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ }
+ }
+ }
+};
diff --git a/src/models/repositories/games/reversi/matching.ts b/src/models/repositories/games/reversi/matching.ts
index 148221dee5..b4515800df 100644
--- a/src/models/repositories/games/reversi/matching.ts
+++ b/src/models/repositories/games/reversi/matching.ts
@@ -3,18 +3,19 @@ import { ReversiMatching } from '@/models/entities/games/reversi/matching';
import { Users } from '../../../index';
import { awaitAll } from '@/prelude/await-all';
import { User } from '@/models/entities/user';
+import { Packed } from '@/misc/schema';
@EntityRepository(ReversiMatching)
export class ReversiMatchingRepository extends Repository<ReversiMatching> {
public async pack(
src: ReversiMatching['id'] | ReversiMatching,
me: { id: User['id'] }
- ) {
+ ): Promise<Packed<'ReversiMatching'>> {
const matching = typeof src === 'object' ? src : await this.findOneOrFail(src);
return await awaitAll({
id: matching.id,
- createdAt: matching.createdAt,
+ createdAt: matching.createdAt.toISOString(),
parentId: matching.parentId,
parent: Users.pack(matching.parentId, me, {
detail: true
@@ -26,3 +27,43 @@ export class ReversiMatchingRepository extends Repository<ReversiMatching> {
});
}
}
+
+export const packedReversiMatchingSchema = {
+ type: 'object' as const,
+ optional: false as const, nullable: false as const,
+ properties: {
+ id: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ format: 'id',
+ example: 'xxxxxxxxxx',
+ },
+ createdAt: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ format: 'date-time',
+ },
+ parentId: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ format: 'id',
+ example: 'xxxxxxxxxx',
+ },
+ parent: {
+ type: 'object' as const,
+ optional: false as const, nullable: true as const,
+ ref: 'User' as const,
+ },
+ childId: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ format: 'id',
+ example: 'xxxxxxxxxx',
+ },
+ child: {
+ type: 'object' as const,
+ optional: false as const, nullable: false as const,
+ ref: 'User' as const,
+ },
+ }
+};
diff --git a/src/models/repositories/hashtag.ts b/src/models/repositories/hashtag.ts
index ee42ad16b6..d52f6ba7c6 100644
--- a/src/models/repositories/hashtag.ts
+++ b/src/models/repositories/hashtag.ts
@@ -1,14 +1,12 @@
import { EntityRepository, Repository } from 'typeorm';
import { Hashtag } from '@/models/entities/hashtag';
-import { SchemaType } from '@/misc/schema';
-
-export type PackedHashtag = SchemaType<typeof packedHashtagSchema>;
+import { Packed } from '@/misc/schema';
@EntityRepository(Hashtag)
export class HashtagRepository extends Repository<Hashtag> {
public async pack(
src: Hashtag,
- ): Promise<PackedHashtag> {
+ ): Promise<Packed<'Hashtag'>> {
return {
tag: src.name,
mentionedUsersCount: src.mentionedUsersCount,
diff --git a/src/models/repositories/messaging-message.ts b/src/models/repositories/messaging-message.ts
index 1a4a8eecc4..abdff63689 100644
--- a/src/models/repositories/messaging-message.ts
+++ b/src/models/repositories/messaging-message.ts
@@ -1,11 +1,9 @@
import { EntityRepository, Repository } from 'typeorm';
import { MessagingMessage } from '@/models/entities/messaging-message';
import { Users, DriveFiles, UserGroups } from '../index';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
import { User } from '@/models/entities/user';
-export type PackedMessagingMessage = SchemaType<typeof packedMessagingMessageSchema>;
-
@EntityRepository(MessagingMessage)
export class MessagingMessageRepository extends Repository<MessagingMessage> {
public validateText(text: string): boolean {
@@ -19,7 +17,7 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> {
populateRecipient?: boolean,
populateGroup?: boolean,
}
- ): Promise<PackedMessagingMessage> {
+ ): Promise<Packed<'MessagingMessage'>> {
const opts = options || {
populateRecipient: true,
populateGroup: true,
@@ -67,7 +65,7 @@ export const packedMessagingMessageSchema = {
},
user: {
type: 'object' as const,
- ref: 'User',
+ ref: 'User' as const,
optional: true as const, nullable: false as const,
},
text: {
@@ -82,7 +80,7 @@ export const packedMessagingMessageSchema = {
file: {
type: 'object' as const,
optional: true as const, nullable: true as const,
- ref: 'DriveFile',
+ ref: 'DriveFile' as const,
},
recipientId: {
type: 'string' as const,
@@ -92,7 +90,7 @@ export const packedMessagingMessageSchema = {
recipient: {
type: 'object' as const,
optional: true as const, nullable: true as const,
- ref: 'User'
+ ref: 'User' as const,
},
groupId: {
type: 'string' as const,
@@ -102,7 +100,7 @@ export const packedMessagingMessageSchema = {
group: {
type: 'object' as const,
optional: true as const, nullable: true as const,
- ref: 'UserGroup'
+ ref: 'UserGroup' as const,
},
isRead: {
type: 'boolean' as const,
diff --git a/src/models/repositories/muting.ts b/src/models/repositories/muting.ts
index e46f4ae448..869afd3c4e 100644
--- a/src/models/repositories/muting.ts
+++ b/src/models/repositories/muting.ts
@@ -2,17 +2,15 @@ import { EntityRepository, Repository } from 'typeorm';
import { Users } from '../index';
import { Muting } from '@/models/entities/muting';
import { awaitAll } from '@/prelude/await-all';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
import { User } from '@/models/entities/user';
-export type PackedMuting = SchemaType<typeof packedMutingSchema>;
-
@EntityRepository(Muting)
export class MutingRepository extends Repository<Muting> {
public async pack(
src: Muting['id'] | Muting,
me?: { id: User['id'] } | null | undefined
- ): Promise<PackedMuting> {
+ ): Promise<Packed<'Muting'>> {
const muting = typeof src === 'object' ? src : await this.findOneOrFail(src);
return await awaitAll({
@@ -56,7 +54,7 @@ export const packedMutingSchema = {
mutee: {
type: 'object' as const,
optional: false as const, nullable: false as const,
- ref: 'User',
+ ref: 'User' as const,
},
}
};
diff --git a/src/models/repositories/note-favorite.ts b/src/models/repositories/note-favorite.ts
index 3248c32ded..47586a9116 100644
--- a/src/models/repositories/note-favorite.ts
+++ b/src/models/repositories/note-favorite.ts
@@ -45,7 +45,7 @@ export const packedNoteFavoriteSchema = {
note: {
type: 'object' as const,
optional: false as const, nullable: false as const,
- ref: 'Note',
+ ref: 'Note' as const,
},
noteId: {
type: 'string' as const,
diff --git a/src/models/repositories/note-reaction.ts b/src/models/repositories/note-reaction.ts
index c349edf182..ba74076f6c 100644
--- a/src/models/repositories/note-reaction.ts
+++ b/src/models/repositories/note-reaction.ts
@@ -1,18 +1,16 @@
import { EntityRepository, Repository } from 'typeorm';
import { NoteReaction } from '@/models/entities/note-reaction';
import { Users } from '../index';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
import { convertLegacyReaction } from '@/misc/reaction-lib';
import { User } from '@/models/entities/user';
-export type PackedNoteReaction = SchemaType<typeof packedNoteReactionSchema>;
-
@EntityRepository(NoteReaction)
export class NoteReactionRepository extends Repository<NoteReaction> {
public async pack(
src: NoteReaction['id'] | NoteReaction,
me?: { id: User['id'] } | null | undefined
- ): Promise<PackedNoteReaction> {
+ ): Promise<Packed<'NoteReaction'>> {
const reaction = typeof src === 'object' ? src : await this.findOneOrFail(src);
return {
@@ -42,7 +40,7 @@ export const packedNoteReactionSchema = {
user: {
type: 'object' as const,
optional: false as const, nullable: false as const,
- ref: 'User',
+ ref: 'User' as const,
},
type: {
type: 'string' as const,
diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts
index 9e0f5e55f0..c0ac22b2db 100644
--- a/src/models/repositories/note.ts
+++ b/src/models/repositories/note.ts
@@ -3,15 +3,13 @@ import * as mfm from 'mfm-js';
import { Note } from '@/models/entities/note';
import { User } from '@/models/entities/user';
import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '../index';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
import { nyaize } from '@/misc/nyaize';
import { awaitAll } from '@/prelude/await-all';
import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@/misc/reaction-lib';
import { NoteReaction } from '@/models/entities/note-reaction';
import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis';
-export type PackedNote = SchemaType<typeof packedNoteSchema>;
-
@EntityRepository(Note)
export class NoteRepository extends Repository<Note> {
public validateCw(x: string) {
@@ -67,7 +65,7 @@ export class NoteRepository extends Repository<Note> {
return true;
}
- private async hideNote(packedNote: PackedNote, meId: User['id'] | null) {
+ private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
let hide = false;
@@ -95,7 +93,7 @@ export class NoteRepository extends Repository<Note> {
hide = true;
} else if (meId === packedNote.userId) {
hide = false;
- } else if (packedNote.reply && (meId === (packedNote.reply as PackedNote).userId)) {
+ } else if (packedNote.reply && (meId === packedNote.reply.userId)) {
// 自分の投稿に対するリプライ
hide = false;
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
@@ -137,7 +135,7 @@ export class NoteRepository extends Repository<Note> {
myReactions: Map<Note['id'], NoteReaction | null>;
};
}
- ): Promise<PackedNote> {
+ ): Promise<Packed<'Note'>> {
const opts = Object.assign({
detail: true,
skipHide: false
@@ -353,7 +351,7 @@ export const packedNoteSchema = {
},
user: {
type: 'object' as const,
- ref: 'User',
+ ref: 'User' as const,
optional: false as const, nullable: false as const,
},
replyId: {
@@ -371,12 +369,12 @@ export const packedNoteSchema = {
reply: {
type: 'object' as const,
optional: true as const, nullable: true as const,
- ref: 'Note'
+ ref: 'Note' as const,
},
renote: {
type: 'object' as const,
optional: true as const, nullable: true as const,
- ref: 'Note'
+ ref: 'Note' as const,
},
viaMobile: {
type: 'boolean' as const,
@@ -423,7 +421,7 @@ export const packedNoteSchema = {
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
- ref: 'DriveFile'
+ ref: 'DriveFile' as const,
}
},
tags: {
@@ -447,11 +445,24 @@ export const packedNoteSchema = {
channel: {
type: 'object' as const,
optional: true as const, nullable: true as const,
- ref: 'Channel'
+ items: {
+ type: 'object' as const,
+ optional: false as const, nullable: false as const,
+ properties: {
+ id: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ },
+ name: {
+ type: 'string' as const,
+ optional: false as const, nullable: true as const,
+ },
+ },
+ },
},
localOnly: {
type: 'boolean' as const,
- optional: false as const, nullable: true as const,
+ optional: true as const, nullable: false as const,
},
emojis: {
type: 'array' as const,
@@ -466,7 +477,7 @@ export const packedNoteSchema = {
},
url: {
type: 'string' as const,
- optional: false as const, nullable: false as const,
+ optional: false as const, nullable: true as const,
},
},
},
@@ -485,11 +496,11 @@ export const packedNoteSchema = {
},
uri: {
type: 'string' as const,
- optional: false as const, nullable: true as const,
+ optional: true as const, nullable: false as const,
},
url: {
type: 'string' as const,
- optional: false as const, nullable: true as const,
+ optional: true as const, nullable: false as const,
},
myReaction: {
diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts
index 55af96b6d7..d1cf9b087e 100644
--- a/src/models/repositories/notification.ts
+++ b/src/models/repositories/notification.ts
@@ -2,13 +2,12 @@ import { EntityRepository, In, Repository } from 'typeorm';
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index';
import { Notification } from '@/models/entities/notification';
import { awaitAll } from '@/prelude/await-all';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
import { Note } from '@/models/entities/note';
import { NoteReaction } from '@/models/entities/note-reaction';
import { User } from '@/models/entities/user';
import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis';
-
-export type PackedNotification = SchemaType<typeof packedNotificationSchema>;
+import { notificationTypes } from '@/types';
@EntityRepository(Notification)
export class NotificationRepository extends Repository<Notification> {
@@ -19,7 +18,7 @@ export class NotificationRepository extends Repository<Notification> {
myReactions: Map<Note['id'], NoteReaction | null>;
};
}
- ): Promise<PackedNotification> {
+ ): Promise<Packed<'Notification'>> {
const notification = typeof src === 'object' ? src : await this.findOneOrFail(src);
const token = notification.appAccessTokenId ? await AccessTokens.findOneOrFail(notification.appAccessTokenId) : null;
@@ -124,19 +123,52 @@ export const packedNotificationSchema = {
optional: false as const, nullable: false as const,
format: 'date-time',
},
+ isRead: {
+ type: 'boolean' as const,
+ optional: false as const, nullable: false as const,
+ },
type: {
type: 'string' as const,
optional: false as const, nullable: false as const,
- enum: ['follow', 'followRequestAccepted', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote'],
+ enum: [...notificationTypes],
+ },
+ user: {
+ type: 'object' as const,
+ ref: 'User' as const,
+ optional: true as const, nullable: true as const,
},
userId: {
type: 'string' as const,
optional: true as const, nullable: true as const,
format: 'id',
},
- user: {
+ note: {
type: 'object' as const,
- ref: 'User',
+ ref: 'Note' as const,
+ optional: true as const, nullable: true as const,
+ },
+ reaction: {
+ type: 'string' as const,
+ optional: true as const, nullable: true as const,
+ },
+ choice: {
+ type: 'number' as const,
+ optional: true as const, nullable: true as const,
+ },
+ invitation: {
+ type: 'object' as const,
+ optional: true as const, nullable: true as const,
+ },
+ body: {
+ type: 'string' as const,
+ optional: true as const, nullable: true as const,
+ },
+ header: {
+ type: 'string' as const,
+ optional: true as const, nullable: true as const,
+ },
+ icon: {
+ type: 'string' as const,
optional: true as const, nullable: true as const,
},
}
diff --git a/src/models/repositories/page.ts b/src/models/repositories/page.ts
index 757aaa5a3f..3a3642d7ec 100644
--- a/src/models/repositories/page.ts
+++ b/src/models/repositories/page.ts
@@ -1,19 +1,17 @@
import { EntityRepository, Repository } from 'typeorm';
import { Page } from '@/models/entities/page';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
import { Users, DriveFiles, PageLikes } from '../index';
import { awaitAll } from '@/prelude/await-all';
import { DriveFile } from '@/models/entities/drive-file';
import { User } from '@/models/entities/user';
-export type PackedPage = SchemaType<typeof packedPageSchema>;
-
@EntityRepository(Page)
export class PageRepository extends Repository<Page> {
public async pack(
src: Page['id'] | Page,
me?: { id: User['id'] } | null | undefined,
- ): Promise<PackedPage> {
+ ): Promise<Packed<'Page'>> {
const meId = me ? me.id : null;
const page = typeof src === 'object' ? src : await this.findOneOrFail(src);
@@ -137,7 +135,7 @@ export const packedPageSchema = {
},
user: {
type: 'object' as const,
- ref: 'User',
+ ref: 'User' as const,
optional: false as const, nullable: false as const,
},
}
diff --git a/src/models/repositories/user-group.ts b/src/models/repositories/user-group.ts
index a76ac7b9d3..b38a2fb50d 100644
--- a/src/models/repositories/user-group.ts
+++ b/src/models/repositories/user-group.ts
@@ -1,15 +1,13 @@
import { EntityRepository, Repository } from 'typeorm';
import { UserGroup } from '@/models/entities/user-group';
import { UserGroupJoinings } from '../index';
-import { SchemaType } from '@/misc/schema';
-
-export type PackedUserGroup = SchemaType<typeof packedUserGroupSchema>;
+import { Packed } from '@/misc/schema';
@EntityRepository(UserGroup)
export class UserGroupRepository extends Repository<UserGroup> {
public async pack(
src: UserGroup['id'] | UserGroup,
- ): Promise<PackedUserGroup> {
+ ): Promise<Packed<'UserGroup'>> {
const userGroup = typeof src === 'object' ? src : await this.findOneOrFail(src);
const users = await UserGroupJoinings.find({
diff --git a/src/models/repositories/user-list.ts b/src/models/repositories/user-list.ts
index 809dbe0268..331c278e6f 100644
--- a/src/models/repositories/user-list.ts
+++ b/src/models/repositories/user-list.ts
@@ -1,15 +1,13 @@
import { EntityRepository, Repository } from 'typeorm';
import { UserList } from '@/models/entities/user-list';
import { UserListJoinings } from '../index';
-import { SchemaType } from '@/misc/schema';
-
-export type PackedUserList = SchemaType<typeof packedUserListSchema>;
+import { Packed } from '@/misc/schema';
@EntityRepository(UserList)
export class UserListRepository extends Repository<UserList> {
public async pack(
src: UserList['id'] | UserList,
- ): Promise<PackedUserList> {
+ ): Promise<Packed<'UserList'>> {
const userList = typeof src === 'object' ? src : await this.findOneOrFail(src);
const users = await UserListJoinings.find({
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
index d4bb995ce2..b6f27e32e2 100644
--- a/src/models/repositories/user.ts
+++ b/src/models/repositories/user.ts
@@ -3,14 +3,12 @@ import { EntityRepository, Repository, In, Not } from 'typeorm';
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '../index';
import config from '@/config/index';
-import { SchemaType } from '@/misc/schema';
+import { Packed } from '@/misc/schema';
import { awaitAll } from '@/prelude/await-all';
import { populateEmojis } from '@/misc/populate-emojis';
import { getAntennas } from '@/misc/antenna-cache';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const';
-export type PackedUser = SchemaType<typeof packedUserSchema>;
-
@EntityRepository(User)
export class UserRepository extends Repository<User> {
public async getRelation(me: User['id'], target: User['id']) {
@@ -164,7 +162,7 @@ export class UserRepository extends Repository<User> {
detail?: boolean,
includeSecrets?: boolean,
}
- ): Promise<PackedUser> {
+ ): Promise<Packed<'User'>> {
const opts = Object.assign({
detail: false,
includeSecrets: false
@@ -375,12 +373,12 @@ export const packedUserSchema = {
},
isAdmin: {
type: 'boolean' as const,
- nullable: false as const, optional: false as const,
+ nullable: false as const, optional: true as const,
default: false
},
isModerator: {
type: 'boolean' as const,
- nullable: false as const, optional: false as const,
+ nullable: false as const, optional: true as const,
default: false
},
isBot: {
@@ -402,23 +400,11 @@ export const packedUserSchema = {
type: 'string' as const,
nullable: false as const, optional: false as const
},
- host: {
- type: 'string' as const,
- nullable: true as const, optional: false as const
- },
url: {
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'url'
},
- aliases: {
- type: 'array' as const,
- nullable: false as const, optional: false as const,
- items: {
- type: 'string' as const,
- nullable: false as const, optional: false as const
- }
- }
}
}
},
@@ -457,7 +443,7 @@ export const packedUserSchema = {
},
isSuspended: {
type: 'boolean' as const,
- nullable: false as const, optional: false as const,
+ nullable: false as const, optional: true as const,
example: false
},
description: {
@@ -476,7 +462,7 @@ export const packedUserSchema = {
},
fields: {
type: 'array' as const,
- nullable: false as const, optional: false as const,
+ nullable: false as const, optional: true as const,
items: {
type: 'object' as const,
nullable: false as const, optional: false as const,
@@ -520,31 +506,31 @@ export const packedUserSchema = {
items: {
type: 'object' as const,
nullable: false as const, optional: false as const,
- ref: 'Note'
+ ref: 'Note' as const,
}
},
pinnedPageId: {
type: 'string' as const,
- nullable: true as const, optional: false as const
+ nullable: true as const, optional: true as const
},
pinnedPage: {
type: 'object' as const,
- nullable: true as const, optional: false as const,
- ref: 'Page'
+ nullable: true as const, optional: true as const,
+ ref: 'Page' as const,
},
twoFactorEnabled: {
type: 'boolean' as const,
- nullable: false as const, optional: false as const,
+ nullable: false as const, optional: true as const,
default: false
},
usePasswordLessLogin: {
type: 'boolean' as const,
- nullable: false as const, optional: false as const,
+ nullable: false as const, optional: true as const,
default: false
},
securityKeys: {
type: 'boolean' as const,
- nullable: false as const, optional: false as const,
+ nullable: false as const, optional: true as const,
default: false
},
avatarId: {
diff --git a/src/queue/index.ts b/src/queue/index.ts
index 4ca7998e61..0ce10a4c60 100644
--- a/src/queue/index.ts
+++ b/src/queue/index.ts
@@ -64,7 +64,9 @@ export function deliver(user: ThinUser, content: unknown, to: string | null) {
if (to == null) return null;
const data = {
- user,
+ user: {
+ id: user.id
+ },
content,
to
};
@@ -171,9 +173,10 @@ export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']
});
}
-export function createDeleteAccountJob(user: ThinUser) {
+export function createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; }) {
return dbQueue.add('deleteAccount', {
- user: user
+ user: user,
+ soft: opts.soft
}, {
removeOnComplete: true,
removeOnFail: true
diff --git a/src/queue/processors/db/delete-account.ts b/src/queue/processors/db/delete-account.ts
index 65327754c2..e54f38e35e 100644
--- a/src/queue/processors/db/delete-account.ts
+++ b/src/queue/processors/db/delete-account.ts
@@ -1,7 +1,7 @@
import * as Bull from 'bull';
import { queueLogger } from '../../logger';
import { DriveFiles, Notes, UserProfiles, Users } from '@/models/index';
-import { DbUserJobData } from '@/queue/types';
+import { DbUserDeleteJobData } from '@/queue/types';
import { Note } from '@/models/entities/note';
import { DriveFile } from '@/models/entities/drive-file';
import { MoreThan } from 'typeorm';
@@ -10,7 +10,7 @@ import { sendEmail } from '@/services/send-email';
const logger = queueLogger.createSubLogger('delete-account');
-export async function deleteAccount(job: Bull.Job<DbUserJobData>): Promise<string | void> {
+export async function deleteAccount(job: Bull.Job<DbUserDeleteJobData>): Promise<string | void> {
logger.info(`Deleting account of ${job.data.user.id} ...`);
const user = await Users.findOne(job.data.user.id);
@@ -83,7 +83,12 @@ export async function deleteAccount(job: Bull.Job<DbUserJobData>): Promise<strin
}
}
- await Users.delete(job.data.user.id);
+ // soft指定されている場合は物理削除しない
+ if (job.data.soft) {
+ // nop
+ } else {
+ await Users.delete(job.data.user.id);
+ }
return 'Account deleted';
}
diff --git a/src/queue/processors/db/export-blocking.ts b/src/queue/processors/db/export-blocking.ts
index a0fc385006..8b8aa259d4 100644
--- a/src/queue/processors/db/export-blocking.ts
+++ b/src/queue/processors/db/export-blocking.ts
@@ -4,7 +4,7 @@ import * as fs from 'fs';
import { queueLogger } from '../../logger';
import addFile from '@/services/drive/add-file';
-import dateFormat from 'dateformat';
+import * as dateFormat from 'dateformat';
import { getFullApAccount } from '@/misc/convert-host';
import { Users, Blockings } from '@/models/index';
import { MoreThan } from 'typeorm';
diff --git a/src/queue/processors/db/export-following.ts b/src/queue/processors/db/export-following.ts
index 3612150363..a0ecf5f560 100644
--- a/src/queue/processors/db/export-following.ts
+++ b/src/queue/processors/db/export-following.ts
@@ -4,7 +4,7 @@ import * as fs from 'fs';
import { queueLogger } from '../../logger';
import addFile from '@/services/drive/add-file';
-import dateFormat from 'dateformat';
+import * as dateFormat from 'dateformat';
import { getFullApAccount } from '@/misc/convert-host';
import { Users, Followings } from '@/models/index';
import { MoreThan } from 'typeorm';
diff --git a/src/queue/processors/db/export-mute.ts b/src/queue/processors/db/export-mute.ts
index 70b2272cdb..d5976f7d56 100644
--- a/src/queue/processors/db/export-mute.ts
+++ b/src/queue/processors/db/export-mute.ts
@@ -4,7 +4,7 @@ import * as fs from 'fs';
import { queueLogger } from '../../logger';
import addFile from '@/services/drive/add-file';
-import dateFormat from 'dateformat';
+import * as dateFormat from 'dateformat';
import { getFullApAccount } from '@/misc/convert-host';
import { Users, Mutings } from '@/models/index';
import { MoreThan } from 'typeorm';
diff --git a/src/queue/processors/db/export-notes.ts b/src/queue/processors/db/export-notes.ts
index 3f146aff1b..49850aa706 100644
--- a/src/queue/processors/db/export-notes.ts
+++ b/src/queue/processors/db/export-notes.ts
@@ -4,7 +4,7 @@ import * as fs from 'fs';
import { queueLogger } from '../../logger';
import addFile from '@/services/drive/add-file';
-import dateFormat from 'dateformat';
+import * as dateFormat from 'dateformat';
import { Users, Notes, Polls } from '@/models/index';
import { MoreThan } from 'typeorm';
import { Note } from '@/models/entities/note';
diff --git a/src/queue/processors/db/export-user-lists.ts b/src/queue/processors/db/export-user-lists.ts
index 89bbd5af18..8a86c4df5d 100644
--- a/src/queue/processors/db/export-user-lists.ts
+++ b/src/queue/processors/db/export-user-lists.ts
@@ -4,7 +4,7 @@ import * as fs from 'fs';
import { queueLogger } from '../../logger';
import addFile from '@/services/drive/add-file';
-import dateFormat from 'dateformat';
+import * as dateFormat from 'dateformat';
import { getFullApAccount } from '@/misc/convert-host';
import { Users, UserLists, UserListJoinings } from '@/models/index';
import { In } from 'typeorm';
diff --git a/src/queue/types.ts b/src/queue/types.ts
index a782fc6b97..39cab29966 100644
--- a/src/queue/types.ts
+++ b/src/queue/types.ts
@@ -17,12 +17,17 @@ export type InboxJobData = {
signature: httpSignature.IParsedSignature;
};
-export type DbJobData = DbUserJobData | DbUserImportJobData;
+export type DbJobData = DbUserJobData | DbUserImportJobData | DbUserDeleteJobData;
export type DbUserJobData = {
user: ThinUser;
};
+export type DbUserDeleteJobData = {
+ user: ThinUser;
+ soft?: boolean;
+};
+
export type DbUserImportJobData = {
user: ThinUser;
fileId: DriveFile['id'];
diff --git a/src/remote/activitypub/kernel/delete/actor.ts b/src/remote/activitypub/kernel/delete/actor.ts
new file mode 100644
index 0000000000..502f8d5ab5
--- /dev/null
+++ b/src/remote/activitypub/kernel/delete/actor.ts
@@ -0,0 +1,26 @@
+import { apLogger } from '../../logger';
+import { createDeleteAccountJob } from '@/queue';
+import { IRemoteUser } from '@/models/entities/user';
+import { Users } from '@/models/index';
+
+const logger = apLogger;
+
+export async function deleteActor(actor: IRemoteUser, uri: string): Promise<string> {
+ logger.info(`Deleting the Actor: ${uri}`);
+
+ if (actor.uri !== uri) {
+ return `skip: delete actor ${actor.uri} !== ${uri}`;
+ }
+
+ if (actor.isDeleted) {
+ logger.info(`skip: already deleted`);
+ }
+
+ const job = await createDeleteAccountJob(actor);
+
+ await Users.update(actor.id, {
+ isDeleted: true,
+ });
+
+ return `ok: queued ${job.name} ${job.id}`;
+}
diff --git a/src/remote/activitypub/kernel/delete/index.ts b/src/remote/activitypub/kernel/delete/index.ts
index 474f3f6d60..86a452de76 100644
--- a/src/remote/activitypub/kernel/delete/index.ts
+++ b/src/remote/activitypub/kernel/delete/index.ts
@@ -2,6 +2,7 @@ import deleteNote from './note';
import { IRemoteUser } from '@/models/entities/user';
import { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '../../type';
import { toSingle } from '@/prelude/array';
+import { deleteActor } from './actor';
/**
* 削除アクティビティを捌きます
@@ -41,7 +42,7 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<string> =>
if (validPost.includes(formarType)) {
return await deleteNote(actor, uri);
} else if (validActor.includes(formarType)) {
- return `Delete Actor is not implanted`;
+ return await deleteActor(actor, uri);
} else {
return `Unknown type ${formarType}`;
}
diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
index 32f3d9ef26..f392a65e3a 100644
--- a/src/remote/activitypub/resolver.ts
+++ b/src/remote/activitypub/resolver.ts
@@ -4,6 +4,8 @@ import { ILocalUser } from '@/models/entities/user';
import { getInstanceActor } from '@/services/instance-actor';
import { signedGet } from './request';
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { extractDbHost } from '@/misc/convert-host';
export default class Resolver {
private history: Set<string>;
@@ -44,6 +46,12 @@ export default class Resolver {
this.history.add(value);
+ const meta = await fetchMeta();
+ const host = extractDbHost(value);
+ if (meta.blockedHosts.includes(host)) {
+ throw new Error('Instance is blocked');
+ }
+
if (config.signToActivityPubGet && !this.user) {
this.user = await getInstanceActor();
}
diff --git a/src/server/api/call.ts b/src/server/api/call.ts
index 2768bde07e..bd86ffdc35 100644
--- a/src/server/api/call.ts
+++ b/src/server/api/call.ts
@@ -40,7 +40,12 @@ export default async (endpoint: string, user: User | null | undefined, token: Ac
}
if (ep.meta.requireCredential && user!.isSuspended) {
- throw new ApiError(accessDenied, { reason: 'Your account has been suspended.' });
+ throw new ApiError({
+ message: 'Your account has been suspended.',
+ code: 'YOUR_ACCOUNT_SUSPENDED',
+ id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370',
+ httpStatusCode: 403
+ });
}
if (ep.meta.requireAdmin && !user!.isAdmin) {
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index 640b14ed6a..6d9d2b0782 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -3,7 +3,7 @@ import { dirname } from 'path';
import { Context } from 'cafy';
import * as path from 'path';
import * as glob from 'glob';
-import { Schema } from '@/misc/schema';
+import { SimpleSchema } from '@/misc/simple-schema';
//const _filename = fileURLToPath(import.meta.url);
const _filename = __filename;
@@ -34,7 +34,7 @@ export interface IEndpointMeta {
};
};
- res?: Schema;
+ res?: SimpleSchema;
/**
* このエンドポイントにリクエストするのにユーザー情報が必須か否か
diff --git a/src/server/api/endpoints/admin/accounts/delete.ts b/src/server/api/endpoints/admin/accounts/delete.ts
new file mode 100644
index 0000000000..4e8a559805
--- /dev/null
+++ b/src/server/api/endpoints/admin/accounts/delete.ts
@@ -0,0 +1,58 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { Users } from '@/models/index';
+import { doPostSuspend } from '@/services/suspend-user';
+import { publishUserEvent } from '@/services/stream';
+import { createDeleteAccountJob } from '@/queue';
+import { ID } from '@/misc/cafy-id';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true as const,
+ requireModerator: true,
+
+ params: {
+ userId: {
+ validator: $.type(ID),
+ },
+ }
+};
+
+export default define(meta, async (ps, me) => {
+ const user = await Users.findOne(ps.userId);
+
+ if (user == null) {
+ throw new Error('user not found');
+ }
+
+ if (user.isAdmin) {
+ throw new Error('cannot suspend admin');
+ }
+
+ if (user.isModerator) {
+ throw new Error('cannot suspend moderator');
+ }
+
+ if (Users.isLocalUser(user)) {
+ // 物理削除する前にDelete activityを送信する
+ await doPostSuspend(user).catch(e => {});
+
+ createDeleteAccountJob(user, {
+ soft: false
+ });
+ } else {
+ createDeleteAccountJob(user, {
+ soft: true // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
+ });
+ }
+
+ await Users.update(user.id, {
+ isDeleted: true,
+ });
+
+ if (Users.isLocalUser(user)) {
+ // Terminate streaming
+ publishUserEvent(user.id, 'terminate', {});
+ }
+});
diff --git a/src/server/api/endpoints/antennas/notes.ts b/src/server/api/endpoints/antennas/notes.ts
index 3c8a4fbdae..1759e95b4c 100644
--- a/src/server/api/endpoints/antennas/notes.ts
+++ b/src/server/api/endpoints/antennas/notes.ts
@@ -1,6 +1,7 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../define';
+import readNote from '@/services/note/read';
import { Antennas, Notes, AntennaNotes } from '@/models/index';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
@@ -84,5 +85,9 @@ export default define(meta, async (ps, user) => {
.take(ps.limit!)
.getMany();
+ if (notes.length > 0) {
+ readNote(user.id, notes);
+ }
+
return await Notes.packMany(notes, user);
});
diff --git a/src/server/api/endpoints/i/delete-account.ts b/src/server/api/endpoints/i/delete-account.ts
index 77f11925cd..10e5adf64a 100644
--- a/src/server/api/endpoints/i/delete-account.ts
+++ b/src/server/api/endpoints/i/delete-account.ts
@@ -35,7 +35,9 @@ export default define(meta, async (ps, user) => {
// 物理削除する前にDelete activityを送信する
await doPostSuspend(user).catch(e => {});
- createDeleteAccountJob(user);
+ createDeleteAccountJob(user, {
+ soft: false
+ });
await Users.update(user.id, {
isDeleted: true,
diff --git a/src/server/api/endpoints/reset-db.ts b/src/server/api/endpoints/reset-db.ts
index f430869302..f0a9dae4ff 100644
--- a/src/server/api/endpoints/reset-db.ts
+++ b/src/server/api/endpoints/reset-db.ts
@@ -18,4 +18,6 @@ export default define(meta, async (ps, user) => {
if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
await resetDb();
+
+ await new Promise(resolve => setTimeout(resolve, 1000));
});
diff --git a/src/server/api/openapi/schemas.ts b/src/server/api/openapi/schemas.ts
index 5402dc6f48..12fc207c47 100644
--- a/src/server/api/openapi/schemas.ts
+++ b/src/server/api/openapi/schemas.ts
@@ -1,26 +1,4 @@
-import { packedUserSchema } from '@/models/repositories/user';
-import { Schema } from '@/misc/schema';
-import { packedNoteSchema } from '@/models/repositories/note';
-import { packedUserListSchema } from '@/models/repositories/user-list';
-import { packedAppSchema } from '@/models/repositories/app';
-import { packedMessagingMessageSchema } from '@/models/repositories/messaging-message';
-import { packedNotificationSchema } from '@/models/repositories/notification';
-import { packedDriveFileSchema } from '@/models/repositories/drive-file';
-import { packedDriveFolderSchema } from '@/models/repositories/drive-folder';
-import { packedFollowingSchema } from '@/models/repositories/following';
-import { packedMutingSchema } from '@/models/repositories/muting';
-import { packedBlockingSchema } from '@/models/repositories/blocking';
-import { packedNoteReactionSchema } from '@/models/repositories/note-reaction';
-import { packedHashtagSchema } from '@/models/repositories/hashtag';
-import { packedPageSchema } from '@/models/repositories/page';
-import { packedUserGroupSchema } from '@/models/repositories/user-group';
-import { packedNoteFavoriteSchema } from '@/models/repositories/note-favorite';
-import { packedChannelSchema } from '@/models/repositories/channel';
-import { packedAntennaSchema } from '@/models/repositories/antenna';
-import { packedClipSchema } from '@/models/repositories/clip';
-import { packedFederationInstanceSchema } from '@/models/repositories/federation-instance';
-import { packedQueueCountSchema } from '@/models/repositories/queue';
-import { packedGalleryPostSchema } from '@/models/repositories/gallery-post';
+import { refs, Schema } from '@/misc/schema';
export function convertSchemaToOpenApiSchema(schema: Schema) {
const res: any = schema;
@@ -72,26 +50,7 @@ export const schemas = {
required: ['error']
},
- User: convertSchemaToOpenApiSchema(packedUserSchema),
- UserList: convertSchemaToOpenApiSchema(packedUserListSchema),
- UserGroup: convertSchemaToOpenApiSchema(packedUserGroupSchema),
- App: convertSchemaToOpenApiSchema(packedAppSchema),
- MessagingMessage: convertSchemaToOpenApiSchema(packedMessagingMessageSchema),
- Note: convertSchemaToOpenApiSchema(packedNoteSchema),
- NoteReaction: convertSchemaToOpenApiSchema(packedNoteReactionSchema),
- NoteFavorite: convertSchemaToOpenApiSchema(packedNoteFavoriteSchema),
- Notification: convertSchemaToOpenApiSchema(packedNotificationSchema),
- DriveFile: convertSchemaToOpenApiSchema(packedDriveFileSchema),
- DriveFolder: convertSchemaToOpenApiSchema(packedDriveFolderSchema),
- Following: convertSchemaToOpenApiSchema(packedFollowingSchema),
- Muting: convertSchemaToOpenApiSchema(packedMutingSchema),
- Blocking: convertSchemaToOpenApiSchema(packedBlockingSchema),
- Hashtag: convertSchemaToOpenApiSchema(packedHashtagSchema),
- Page: convertSchemaToOpenApiSchema(packedPageSchema),
- Channel: convertSchemaToOpenApiSchema(packedChannelSchema),
- QueueCount: convertSchemaToOpenApiSchema(packedQueueCountSchema),
- Antenna: convertSchemaToOpenApiSchema(packedAntennaSchema),
- Clip: convertSchemaToOpenApiSchema(packedClipSchema),
- FederationInstance: convertSchemaToOpenApiSchema(packedFederationInstanceSchema),
- GalleryPost: convertSchemaToOpenApiSchema(packedGalleryPostSchema),
+ ...Object.fromEntries(
+ Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema)])
+ ),
};
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index fff1037ff9..83c3dfee94 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -18,6 +18,11 @@ export default async (ctx: Koa.Context) => {
const password = body['password'];
const token = body['token'];
+ function error(status: number, error: { id: string }) {
+ ctx.status = status;
+ ctx.body = { error };
+ }
+
if (typeof username != 'string') {
ctx.status = 400;
return;
@@ -40,15 +45,15 @@ export default async (ctx: Koa.Context) => {
}) as ILocalUser;
if (user == null) {
- ctx.throw(404, {
- error: 'user not found'
+ error(404, {
+ id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
});
return;
}
if (user.isSuspended) {
- ctx.throw(403, {
- error: 'user is suspended'
+ error(403, {
+ id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
});
return;
}
@@ -58,7 +63,7 @@ export default async (ctx: Koa.Context) => {
// Compare password
const same = await bcrypt.compare(password, profile.password!);
- async function fail(status?: number, failure?: { error: string }) {
+ async function fail(status?: number, failure?: { id: string }) {
// Append signin history
await Signins.insert({
id: genId(),
@@ -69,7 +74,7 @@ export default async (ctx: Koa.Context) => {
success: false
});
- ctx.throw(status || 500, failure || { error: 'someting happened' });
+ error(status || 500, failure || { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
}
if (!profile.twoFactorEnabled) {
@@ -78,7 +83,7 @@ export default async (ctx: Koa.Context) => {
return;
} else {
await fail(403, {
- error: 'incorrect password'
+ id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
});
return;
}
@@ -87,7 +92,7 @@ export default async (ctx: Koa.Context) => {
if (token) {
if (!same) {
await fail(403, {
- error: 'incorrect password'
+ id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
});
return;
}
@@ -104,14 +109,14 @@ export default async (ctx: Koa.Context) => {
return;
} else {
await fail(403, {
- error: 'invalid token'
+ id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f'
});
return;
}
} else if (body.credentialId) {
if (!same && !profile.usePasswordLessLogin) {
await fail(403, {
- error: 'incorrect password'
+ id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
});
return;
}
@@ -127,7 +132,7 @@ export default async (ctx: Koa.Context) => {
if (!challenge) {
await fail(403, {
- error: 'non-existent challenge'
+ id: '2715a88a-2125-4013-932f-aa6fe72792da'
});
return;
}
@@ -139,7 +144,7 @@ export default async (ctx: Koa.Context) => {
if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) {
await fail(403, {
- error: 'non-existent challenge'
+ id: '2715a88a-2125-4013-932f-aa6fe72792da'
});
return;
}
@@ -155,7 +160,7 @@ export default async (ctx: Koa.Context) => {
if (!securityKey) {
await fail(403, {
- error: 'invalid credentialId'
+ id: '66269679-aeaf-4474-862b-eb761197e046'
});
return;
}
@@ -174,14 +179,14 @@ export default async (ctx: Koa.Context) => {
return;
} else {
await fail(403, {
- error: 'invalid challenge data'
+ id: '93b86c4b-72f9-40eb-9815-798928603d1e'
});
return;
}
} else {
if (!same && !profile.usePasswordLessLogin) {
await fail(403, {
- error: 'incorrect password'
+ id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
});
return;
}
@@ -192,7 +197,7 @@ export default async (ctx: Koa.Context) => {
if (keys.length === 0) {
await fail(403, {
- error: 'no keys found'
+ id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4'
});
return;
}
diff --git a/src/server/api/stream/channels/channel.ts b/src/server/api/stream/channels/channel.ts
index e6a9a6c696..72ddbf93b4 100644
--- a/src/server/api/stream/channels/channel.ts
+++ b/src/server/api/stream/channels/channel.ts
@@ -3,8 +3,8 @@ import Channel from '../channel';
import { Notes, Users } from '@/models/index';
import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
-import { PackedNote } from '@/models/repositories/note';
import { User } from '@/models/entities/user';
+import { Packed } from '@/misc/schema';
export default class extends Channel {
public readonly chName = 'channel';
@@ -25,7 +25,7 @@ export default class extends Channel {
}
@autobind
- private async onNote(note: PackedNote) {
+ private async onNote(note: Packed<'Note'>) {
if (note.channelId !== this.channelId) return;
// リプライなら再pack
diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts
index 2cb138966f..f5983ab472 100644
--- a/src/server/api/stream/channels/global-timeline.ts
+++ b/src/server/api/stream/channels/global-timeline.ts
@@ -3,9 +3,9 @@ import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import Channel from '../channel';
import { fetchMeta } from '@/misc/fetch-meta';
import { Notes } from '@/models/index';
-import { PackedNote } from '@/models/repositories/note';
import { checkWordMute } from '@/misc/check-word-mute';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
export default class extends Channel {
public readonly chName = 'globalTimeline';
@@ -24,7 +24,7 @@ export default class extends Channel {
}
@autobind
- private async onNote(note: PackedNote) {
+ private async onNote(note: Packed<'Note'>) {
if (note.visibility !== 'public') return;
if (note.channelId != null) return;
@@ -43,7 +43,7 @@ export default class extends Channel {
// 関係ない返信は除外
if (note.reply) {
- const reply = note.reply as PackedNote;
+ const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
}
diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts
index 997ab75f6d..281be4f2eb 100644
--- a/src/server/api/stream/channels/hashtag.ts
+++ b/src/server/api/stream/channels/hashtag.ts
@@ -2,9 +2,9 @@ import autobind from 'autobind-decorator';
import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import Channel from '../channel';
import { Notes } from '@/models/index';
-import { PackedNote } from '@/models/repositories/note';
import { normalizeForSearch } from '@/misc/normalize-for-search';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
export default class extends Channel {
public readonly chName = 'hashtag';
@@ -23,7 +23,7 @@ export default class extends Channel {
}
@autobind
- private async onNote(note: PackedNote) {
+ private async onNote(note: Packed<'Note'>) {
const noteTags = note.tags ? note.tags.map((t: string) => t.toLowerCase()) : [];
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
if (!matched) return;
diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts
index c7a9728741..52e9aec250 100644
--- a/src/server/api/stream/channels/home-timeline.ts
+++ b/src/server/api/stream/channels/home-timeline.ts
@@ -2,9 +2,9 @@ import autobind from 'autobind-decorator';
import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import Channel from '../channel';
import { Notes } from '@/models/index';
-import { PackedNote } from '@/models/repositories/note';
import { checkWordMute } from '@/misc/check-word-mute';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
export default class extends Channel {
public readonly chName = 'homeTimeline';
@@ -18,7 +18,7 @@ export default class extends Channel {
}
@autobind
- private async onNote(note: PackedNote) {
+ private async onNote(note: Packed<'Note'>) {
if (note.channelId) {
if (!this.followingChannels.has(note.channelId)) return;
} else {
@@ -51,7 +51,7 @@ export default class extends Channel {
// 関係ない返信は除外
if (note.reply) {
- const reply = note.reply as PackedNote;
+ const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
}
diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts
index 5c454764ec..51f95fc0cd 100644
--- a/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/src/server/api/stream/channels/hybrid-timeline.ts
@@ -3,10 +3,9 @@ import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import Channel from '../channel';
import { fetchMeta } from '@/misc/fetch-meta';
import { Notes } from '@/models/index';
-import { PackedNote } from '@/models/repositories/note';
-import { PackedUser } from '@/models/repositories/user';
import { checkWordMute } from '@/misc/check-word-mute';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
export default class extends Channel {
public readonly chName = 'hybridTimeline';
@@ -23,7 +22,7 @@ export default class extends Channel {
}
@autobind
- private async onNote(note: PackedNote) {
+ private async onNote(note: Packed<'Note'>) {
// チャンネルの投稿ではなく、自分自身の投稿 または
// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
// チャンネルの投稿ではなく、全体公開のローカルの投稿 または
@@ -31,7 +30,7 @@ export default class extends Channel {
if (!(
(note.channelId == null && this.user!.id === note.userId) ||
(note.channelId == null && this.following.has(note.userId)) ||
- (note.channelId == null && ((note.user as PackedUser).host == null && note.visibility === 'public')) ||
+ (note.channelId == null && (note.user.host == null && note.visibility === 'public')) ||
(note.channelId != null && this.followingChannels.has(note.channelId))
)) return;
@@ -60,7 +59,7 @@ export default class extends Channel {
// 関係ない返信は除外
if (note.reply) {
- const reply = note.reply as PackedNote;
+ const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
}
diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts
index 4bf0d02ed3..a6166c2be2 100644
--- a/src/server/api/stream/channels/local-timeline.ts
+++ b/src/server/api/stream/channels/local-timeline.ts
@@ -3,10 +3,9 @@ import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import Channel from '../channel';
import { fetchMeta } from '@/misc/fetch-meta';
import { Notes } from '@/models/index';
-import { PackedNote } from '@/models/repositories/note';
-import { PackedUser } from '@/models/repositories/user';
import { checkWordMute } from '@/misc/check-word-mute';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
export default class extends Channel {
public readonly chName = 'localTimeline';
@@ -25,8 +24,8 @@ export default class extends Channel {
}
@autobind
- private async onNote(note: PackedNote) {
- if ((note.user as PackedUser).host !== null) return;
+ private async onNote(note: Packed<'Note'>) {
+ if (note.user.host !== null) return;
if (note.visibility !== 'public') return;
if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;
@@ -45,7 +44,7 @@ export default class extends Channel {
// 関係ない返信は除外
if (note.reply) {
- const reply = note.reply as PackedNote;
+ const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
}
diff --git a/src/server/api/stream/channels/user-list.ts b/src/server/api/stream/channels/user-list.ts
index 0ca83cd658..63b254605b 100644
--- a/src/server/api/stream/channels/user-list.ts
+++ b/src/server/api/stream/channels/user-list.ts
@@ -3,8 +3,8 @@ import Channel from '../channel';
import { Notes, UserListJoinings, UserLists } from '@/models/index';
import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import { User } from '@/models/entities/user';
-import { PackedNote } from '@/models/repositories/note';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
export default class extends Channel {
public readonly chName = 'userList';
@@ -47,7 +47,7 @@ export default class extends Channel {
}
@autobind
- private async onNote(note: PackedNote) {
+ private async onNote(note: Packed<'Note'>) {
if (!this.listUsers.includes(note.userId)) return;
if (['followers', 'specified'].includes(note.visibility)) {
diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts
index 469f28f11c..ccd555e149 100644
--- a/src/server/api/stream/index.ts
+++ b/src/server/api/stream/index.ts
@@ -14,7 +14,7 @@ import { AccessToken } from '@/models/entities/access-token';
import { UserProfile } from '@/models/entities/user-profile';
import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream';
import { UserGroup } from '@/models/entities/user-group';
-import { PackedNote } from '@/models/repositories/note';
+import { Packed } from '@/misc/schema';
/**
* Main stream connection
@@ -31,7 +31,7 @@ export default class Connection {
public subscriber: EventEmitter;
private channels: Channel[] = [];
private subscribingNotes: any = {};
- private cachedNotes: PackedNote[] = [];
+ private cachedNotes: Packed<'Note'>[] = [];
constructor(
wsConnection: websocket.connection,
@@ -150,8 +150,8 @@ export default class Connection {
}
@autobind
- public cacheNote(note: PackedNote) {
- const add = (note: PackedNote) => {
+ public cacheNote(note: Packed<'Note'>) {
+ const add = (note: Packed<'Note'>) => {
const existIndex = this.cachedNotes.findIndex(n => n.id === note.id);
if (existIndex > -1) {
this.cachedNotes[existIndex] = note;
@@ -165,8 +165,8 @@ export default class Connection {
};
add(note);
- if (note.reply) add(note.reply as PackedNote);
- if (note.renote) add(note.renote as PackedNote);
+ if (note.reply) add(note.reply);
+ if (note.renote) add(note.renote);
}
@autobind
diff --git a/src/server/web/boot.js b/src/server/web/boot.js
index a3a0685669..d4a2529e63 100644
--- a/src/server/web/boot.js
+++ b/src/server/web/boot.js
@@ -48,8 +48,8 @@
} else if (localeOutdated) {
// nop
} else {
+ await checkUpdate();
renderError('LOCALE_FETCH_FAILED');
- checkUpdate();
return;
}
}
@@ -65,8 +65,8 @@
script.setAttribute('async', 'true');
script.setAttribute('defer', 'true');
script.addEventListener('error', async () => {
+ await checkUpdate();
renderError('APP_FETCH_FAILED');
- checkUpdate();
});
document.head.appendChild(script);
//#endregion
@@ -142,10 +142,6 @@
if (meta.version != v) {
localStorage.setItem('v', meta.version);
- alert(
- 'Misskeyの新しいバージョンがあります。ページを再度読み込みします。' +
- '\n\n' +
- 'New version of Misskey available. The page will be reloaded.');
refresh();
}
}
diff --git a/src/services/chart/core.ts b/src/services/chart/core.ts
index eee7d20efb..c0d3280c2b 100644
--- a/src/services/chart/core.ts
+++ b/src/services/chart/core.ts
@@ -7,7 +7,7 @@
import * as nestedProperty from 'nested-property';
import autobind from 'autobind-decorator';
import Logger from '../logger';
-import { Schema } from '@/misc/schema';
+import { SimpleSchema } from '@/misc/simple-schema';
import { EntitySchema, getRepository, Repository, LessThan, Between } from 'typeorm';
import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/prelude/time';
import { getChartInsertLock } from '@/misc/app-lock';
@@ -56,7 +56,7 @@ export default abstract class Chart<T extends Record<string, any>> {
diff: DeepPartial<T>;
group: string | null;
}[] = [];
- public schema: Schema;
+ public schema: SimpleSchema;
protected repository: Repository<Log>;
protected abstract genNewLog(latest: T): DeepPartial<T>;
@@ -69,7 +69,7 @@ export default abstract class Chart<T extends Record<string, any>> {
protected abstract fetchActual(group: string | null): Promise<DeepPartial<T>>;
@autobind
- private static convertSchemaToFlatColumnDefinitions(schema: Schema) {
+ private static convertSchemaToFlatColumnDefinitions(schema: SimpleSchema) {
const columns = {} as any;
const flatColumns = (x: Obj, path?: string) => {
for (const [k, v] of Object.entries(x)) {
@@ -181,7 +181,7 @@ export default abstract class Chart<T extends Record<string, any>> {
}
@autobind
- public static schemaToEntity(name: string, schema: Schema): EntitySchema {
+ public static schemaToEntity(name: string, schema: SimpleSchema): EntitySchema {
return new EntitySchema({
name: `__chart__${camelToSnake(name)}`,
columns: {
@@ -211,7 +211,7 @@ export default abstract class Chart<T extends Record<string, any>> {
});
}
- constructor(name: string, schema: Schema, grouped = false) {
+ constructor(name: string, schema: SimpleSchema, grouped = false) {
this.name = name;
this.schema = schema;
const entity = Chart.schemaToEntity(name, schema);
@@ -546,8 +546,8 @@ export default abstract class Chart<T extends Record<string, any>> {
}
}
-export function convertLog(logSchema: Schema): Schema {
- const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy
+export function convertLog(logSchema: SimpleSchema): SimpleSchema {
+ const v: SimpleSchema = JSON.parse(JSON.stringify(logSchema)); // copy
if (v.type === 'number') {
v.type = 'array';
v.items = {
diff --git a/src/services/note/read.ts b/src/services/note/read.ts
index 2e221d553a..f25f86da9c 100644
--- a/src/services/note/read.ts
+++ b/src/services/note/read.ts
@@ -6,15 +6,15 @@ import { Not, IsNull, In } from 'typeorm';
import { Channel } from '@/models/entities/channel';
import { checkHitAntenna } from '@/misc/check-hit-antenna';
import { getAntennas } from '@/misc/antenna-cache';
-import { PackedNote } from '@/models/repositories/note';
import { readNotificationByQuery } from '@/server/api/common/read-notification';
+import { Packed } from '@/misc/schema';
/**
* Mark notes as read
*/
export default async function(
userId: User['id'],
- notes: (Note | PackedNote)[],
+ notes: (Note | Packed<'Note'>)[],
info?: {
following: Set<User['id']>;
followingChannels: Set<Channel['id']>;
@@ -34,10 +34,10 @@ export default async function(
})).map(x => x.followeeId));
const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
- const readMentions: (Note | PackedNote)[] = [];
- const readSpecifiedNotes: (Note | PackedNote)[] = [];
- const readChannelNotes: (Note | PackedNote)[] = [];
- const readAntennaNotes: (Note | PackedNote)[] = [];
+ const readMentions: (Note | Packed<'Note'>)[] = [];
+ const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
+ const readChannelNotes: (Note | Packed<'Note'>)[] = [];
+ const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) {
@@ -52,7 +52,7 @@ export default async function(
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
for (const antenna of myAntennas) {
- if (checkHitAntenna(antenna, note, note.user as any, undefined, Array.from(following))) {
+ if (await checkHitAntenna(antenna, note, note.user as any, undefined, Array.from(following))) {
readAntennaNotes.push(note);
}
}
diff --git a/src/services/push-notification.ts b/src/services/push-notification.ts
index 5bd7499692..5949d11b3b 100644
--- a/src/services/push-notification.ts
+++ b/src/services/push-notification.ts
@@ -2,11 +2,10 @@ import * as push from 'web-push';
import config from '@/config/index';
import { SwSubscriptions } from '@/models/index';
import { fetchMeta } from '@/misc/fetch-meta';
-import { PackedNotification } from '../models/repositories/notification';
-import { PackedMessagingMessage } from '../models/repositories/messaging-message';
+import { Packed } from '@/misc/schema';
type notificationType = 'notification' | 'unreadMessagingMessage';
-type notificationBody = PackedNotification | PackedMessagingMessage;
+type notificationBody = Packed<'Notification'> | Packed<'MessagingMessage'>;
export default async function(userId: string, type: notificationType, body: notificationBody) {
const meta = await fetchMeta();