From 052f8b265d9a4facebbdda7a81cd60d9829dd1e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= Date: Mon, 13 May 2019 18:03:29 +0900 Subject: [PATCH 001/153] Update README.md [AUTOGEN] (#4916) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b0ef6950a..363bb068b 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md). Atsuko Tominaga natalie Hiratake +noellabo CG Hekovic dansup @@ -157,6 +158,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md). Atsuko Tominaga natalie Hiratake +noellabo CG Hekovic dansup @@ -171,7 +173,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md). Takashi Shibuya -**Last updated:** Tue, 07 May 2019 11:55:07 UTC +**Last updated:** Mon, 13 May 2019 06:13:06 UTC :four_leaf_clover: Copyright From 342e48ed77a54dbbaad86fe0a8c7962d70df7607 Mon Sep 17 00:00:00 2001 From: MeiMei <30769358+mei23@users.noreply.github.com> Date: Tue, 14 May 2019 02:50:23 +0900 Subject: [PATCH 002/153] Fix meta tags (#4918) --- src/client/app/admin/views/federation.vue | 2 +- src/client/app/common/views/components/forkit.vue | 2 +- .../common/views/components/integrations.integration.vue | 2 +- .../app/common/views/components/messaging-room.message.vue | 2 +- src/client/app/common/views/components/mfm.ts | 2 ++ src/client/app/common/views/components/nav.vue | 4 ++-- src/client/app/common/views/components/settings/2fa.vue | 2 +- .../app/common/views/components/settings/integration.vue | 6 +++--- src/client/app/common/views/components/settings/theme.vue | 2 +- src/client/app/common/views/components/url-preview.vue | 2 +- src/client/app/common/views/components/url.vue | 4 ++-- src/client/app/common/views/deck/deck.note-column.vue | 2 +- src/client/app/common/views/deck/deck.user-column.vue | 2 +- src/client/app/common/views/widgets/rss.vue | 2 +- src/client/app/desktop/views/components/note-detail.vue | 2 +- src/client/app/desktop/views/components/note.vue | 2 +- src/client/app/desktop/views/home/user/index.vue | 2 +- src/client/app/mobile/views/components/media-video.vue | 1 + src/client/app/mobile/views/components/note-detail.vue | 2 +- src/client/app/mobile/views/components/note.vue | 2 +- src/client/app/mobile/views/pages/user/index.vue | 2 +- src/docs/article.pug | 2 +- src/server/web/views/note.pug | 3 +++ src/server/web/views/user.pug | 3 +++ 24 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/client/app/admin/views/federation.vue b/src/client/app/admin/views/federation.vue index 6b09a3c28..25400393b 100644 --- a/src/client/app/admin/views/federation.vue +++ b/src/client/app/admin/views/federation.vue @@ -130,7 +130,7 @@ {{ $t('status') }}
- {{ instance.host }} + {{ instance.host }} {{ instance.notesCount | number }} {{ instance.usersCount | number }} {{ instance.followingCount | number }} diff --git a/src/client/app/common/views/components/forkit.vue b/src/client/app/common/views/components/forkit.vue index 5629c5ac2..d652b846a 100644 --- a/src/client/app/common/views/components/forkit.vue +++ b/src/client/app/common/views/components/forkit.vue @@ -1,5 +1,5 @@ diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue index 256ea760b..908533e0c 100644 --- a/src/client/app/common/views/components/messaging-room.message.vue +++ b/src/client/app/common/views/components/messaging-room.message.vue @@ -9,7 +9,7 @@
- +

{{ message.file.name }}

diff --git a/src/client/app/common/views/components/mfm.ts b/src/client/app/common/views/components/mfm.ts index 78734200a..945917288 100644 --- a/src/client/app/common/views/components/mfm.ts +++ b/src/client/app/common/views/components/mfm.ts @@ -174,6 +174,7 @@ export default Vue.component('misskey-flavored-markdown', { key: Math.random(), props: { url: token.node.props.url, + rel: 'nofollow noopener', target: '_blank' }, attrs: { @@ -187,6 +188,7 @@ export default Vue.component('misskey-flavored-markdown', { attrs: { class: 'link', href: token.node.props.url, + rel: 'nofollow noopener', target: '_blank', title: token.node.props.url, style: 'color:var(--mfmLink);' diff --git a/src/client/app/common/views/components/nav.vue b/src/client/app/common/views/components/nav.vue index a8f9bb928..9d4e7091b 100644 --- a/src/client/app/common/views/components/nav.vue +++ b/src/client/app/common/views/components/nav.vue @@ -2,9 +2,9 @@
{{ $t('about') }} - {{ $t('repository') }} + {{ $t('repository') }} - {{ $t('feedback') }} + {{ $t('feedback') }} {{ $t('develop') }} diff --git a/src/client/app/common/views/components/settings/2fa.vue b/src/client/app/common/views/components/settings/2fa.vue index 07a149315..6e8d19d83 100644 --- a/src/client/app/common/views/components/settings/2fa.vue +++ b/src/client/app/common/views/components/settings/2fa.vue @@ -9,7 +9,7 @@
    -
  1. {{ $t('authenticator') }}{{ $t('howtoinstall') }}
  2. +
  3. {{ $t('authenticator') }}{{ $t('howtoinstall') }}
  4. {{ $t('scan') }}
  5. {{ $t('done') }}
    {{ $t('token') }} diff --git a/src/client/app/common/views/components/settings/integration.vue b/src/client/app/common/views/components/settings/integration.vue index 8ac0c134c..71ad8b450 100644 --- a/src/client/app/common/views/components/settings/integration.vue +++ b/src/client/app/common/views/components/settings/integration.vue @@ -4,21 +4,21 @@
    Twitter
    -

    {{ $t('connected-to') }}: @{{ $store.state.i.twitter.screenName }}

    +

    {{ $t('connected-to') }}: @{{ $store.state.i.twitter.screenName }}

    {{ $t('disconnect') }} {{ $t('connect') }}
    Discord
    -

    {{ $t('connected-to') }}: @{{ $store.state.i.discord.username }}#{{ $store.state.i.discord.discriminator }}

    +

    {{ $t('connected-to') }}: @{{ $store.state.i.discord.username }}#{{ $store.state.i.discord.discriminator }}

    {{ $t('disconnect') }} {{ $t('connect') }}
    GitHub
    -

    {{ $t('connected-to') }}: @{{ $store.state.i.github.login }}

    +

    {{ $t('connected-to') }}: @{{ $store.state.i.github.login }}

    {{ $t('disconnect') }} {{ $t('connect') }}
    diff --git a/src/client/app/common/views/components/settings/theme.vue b/src/client/app/common/views/components/settings/theme.vue index b1f7b2bc4..3e6b9133c 100644 --- a/src/client/app/common/views/components/settings/theme.vue +++ b/src/client/app/common/views/components/settings/theme.vue @@ -45,7 +45,7 @@ - {{ $t('find-more-theme') }} + {{ $t('find-more-theme') }}
    {{ $t('create-a-theme') }} diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue index af5f3e4eb..9ca4497ad 100644 --- a/src/client/app/common/views/components/url-preview.vue +++ b/src/client/app/common/views/components/url-preview.vue @@ -9,7 +9,7 @@
- +
diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue index 2daf79cba..faf439814 100644 --- a/src/client/app/common/views/components/url.vue +++ b/src/client/app/common/views/components/url.vue @@ -1,5 +1,5 @@

{{ $t('@.user-suspended') }}

-
+

{{ $t('@.is-remote-user') }}{{ $t('@.view-on-remote') }}

diff --git a/src/docs/article.pug b/src/docs/article.pug index 38494fec6..48f501d40 100644 --- a/src/docs/article.pug +++ b/src/docs/article.pug @@ -6,4 +6,4 @@ block main block footer p = i18n('docs.edit-this-page-on-github') - a(href=src target="_blank")= i18n('docs.edit-this-page-on-github-link') + a(href=src rel="noopener" target="_blank")= i18n('docs.edit-this-page-on-github-link') diff --git a/src/server/web/views/note.pug b/src/server/web/views/note.pug index 983c731a0..0580e959f 100644 --- a/src/server/web/views/note.pug +++ b/src/server/web/views/note.pug @@ -26,6 +26,9 @@ block meta meta(name='twitter:card' content='summary') // todo + if user.host + meta(name='robots' content='noindex') + if user.twitter meta(name='twitter:creator' content=`@${user.twitter.screenName}`) diff --git a/src/server/web/views/user.pug b/src/server/web/views/user.pug index bff98ba80..9b257afb7 100644 --- a/src/server/web/views/user.pug +++ b/src/server/web/views/user.pug @@ -24,6 +24,9 @@ block meta meta(name='twitter:card' content='summary') + if user.host + meta(name='robots' content='noindex') + if profile.twitter meta(name='twitter:creator' content=`@${profile.twitter.screenName}`) From b128b593c205df895d8d9d3c5763d87f7e7ebd71 Mon Sep 17 00:00:00 2001 From: MeiMei <30769358+mei23@users.noreply.github.com> Date: Tue, 14 May 2019 02:53:05 +0900 Subject: [PATCH 003/153] Fix: user menu (#4845) (#4920) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: Firefoxで自分のメニューが開けないなど * 自分のユーザーメニューにはミュートなどを表示しないようになど --- .../app/common/views/components/user-menu.vue | 31 ++++++++++--------- .../desktop/views/home/user/user.header.vue | 13 +------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/client/app/common/views/components/user-menu.vue b/src/client/app/common/views/components/user-menu.vue index 0af0fdb7e..7cbffa9f9 100644 --- a/src/client/app/common/views/components/user-menu.vue +++ b/src/client/app/common/views/components/user-menu.vue @@ -7,7 +7,6 @@ diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue index 9b1df7977..bf8e5d25b 100644 --- a/src/client/app/common/views/components/signup.vue +++ b/src/client/app/common/views/components/signup.vue @@ -37,8 +37,13 @@

{{ $t('password-not-matched') }}

+ + + {{ $t('tos') }} + +
- {{ $t('create') }} + {{ $t('create') }} @@ -64,7 +69,8 @@ export default Vue.extend({ usernameState: null, passwordStrength: '', passwordRetypeState: null, - meta: null + meta: {}, + ToSAgreement: false } }, diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts index 2c36b8333..c3797a9ed 100644 --- a/src/models/entities/meta.ts +++ b/src/models/entities/meta.ts @@ -268,4 +268,24 @@ export class Meta { nullable: true }) public discordClientSecret: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public ToSUrl: string | null; + + @Column('varchar', { + length: 512, + default: 'https://github.com/syuilo/misskey', + nullable: false + }) + public repositoryUrl: string; + + @Column('varchar', { + length: 512, + default: 'https://github.com/syuilo/misskey/issues/new', + nullable: true + }) + public feedbackUrl: string | null; } diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index 8a2019fcc..e34840e90 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -330,6 +330,27 @@ export const meta = { 'ja-JP': 'ServiceWorkerのVAPIDキーペアの秘密鍵' } }, + + ToSUrl: { + validator: $.optional.nullable.str, + desc: { + 'ja-JP': '利用規約のURL' + } + }, + + repositoryUrl: { + validator: $.optional.str, + desc: { + 'ja-JP': 'リポジトリのURL' + } + }, + + feedbackUrl: { + validator: $.optional.str, + desc: { + 'ja-JP': 'フィードバックのURL' + } + } } }; @@ -516,6 +537,18 @@ export default define(meta, async (ps) => { set.swPrivateKey = ps.swPrivateKey; } + if (ps.ToSUrl !== undefined) { + set.ToSUrl = ps.ToSUrl; + } + + if (ps.repositoryUrl !== undefined) { + set.repositoryUrl = ps.repositoryUrl; + } + + if (ps.feedbackUrl !== undefined) { + set.feedbackUrl = ps.feedbackUrl; + } + await getConnection().transaction(async transactionalEntityManager => { const meta = await transactionalEntityManager.findOne(Meta, { order: { diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 5667e7fbb..e29edae26 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -106,6 +106,9 @@ export default define(meta, async (ps, me) => { uri: config.url, description: instance.description, langs: instance.langs, + ToSUrl: instance.ToSUrl, + repositoryUrl: instance.repositoryUrl, + feedbackUrl: instance.feedbackUrl, secure: config.https != null, machine: os.hostname(), diff --git a/src/server/nodeinfo.ts b/src/server/nodeinfo.ts index d3ad90fab..da1e4e7b5 100644 --- a/src/server/nodeinfo.ts +++ b/src/server/nodeinfo.ts @@ -26,6 +26,9 @@ const nodeinfo2 = async () => { maintainerName, maintainerEmail, langs, + ToSUrl, + repositoryUrl, + feedbackUrl, announcements, disableRegistration, disableLocalTimeline, @@ -77,6 +80,9 @@ const nodeinfo2 = async () => { email: maintainerEmail }, langs, + ToSUrl, + repositoryUrl, + feedbackUrl, announcements, disableRegistration, disableLocalTimeline, From 5d42ee2359c1f9a0018fc3ae9c7b3f2f2c6fbccc Mon Sep 17 00:00:00 2001 From: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com> Date: Tue, 14 May 2019 10:50:20 +0900 Subject: [PATCH 005/153] Fix tag cloud on Welcome page (#4922) Resolve #4754 --- src/client/app/common/views/components/tag-cloud.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/app/common/views/components/tag-cloud.vue b/src/client/app/common/views/components/tag-cloud.vue index b5eef964b..3fa5e3b9d 100644 --- a/src/client/app/common/views/components/tag-cloud.vue +++ b/src/client/app/common/views/components/tag-cloud.vue @@ -4,7 +4,7 @@

{{ $t('empty') }}

@@ -149,7 +158,7 @@ import Vue from 'vue'; import i18n from '../../i18n'; import { faPaperPlane } from '@fortawesome/free-regular-svg-icons'; -import { faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faTrafficLight, faInbox } from '@fortawesome/free-solid-svg-icons'; +import { faBan, faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faTrafficLight, faInbox } from '@fortawesome/free-solid-svg-icons'; import ApexCharts from 'apexcharts'; import * as tinycolor from 'tinycolor2'; @@ -176,7 +185,8 @@ export default Vue.extend({ chartSrc: 'requests', chartSpan: 'hour', chartInstance: null, - faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faPaperPlane, faTrafficLight, faInbox + blockedHosts: '', + faBan, faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faPaperPlane, faTrafficLight, faInbox }; }, @@ -246,6 +256,10 @@ export default Vue.extend({ mounted() { this.fetchInstances(); + + this.$root.getMeta().then(meta => { + this.blockedHosts = meta.blockedHosts.join('\n'); + }); }, beforeDestroy() { @@ -477,6 +491,22 @@ export default Vue.extend({ }] }; }, + + saveBlockedHosts() { + this.$root.api('admin/update-meta', { + blockedHosts: this.blockedHosts.split('\n') + }).then(() => { + this.$root.dialog({ + type: 'success', + text: this.$t('saved') + }); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + } } }); diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index e34840e90..e4f2e86aa 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -70,6 +70,13 @@ export const meta = { } }, + blockedHosts: { + validator: $.optional.nullable.arr($.str), + desc: { + 'ja-JP': 'ブロックするホスト' + } + }, + mascotImageUrl: { validator: $.optional.nullable.str, desc: { @@ -389,6 +396,10 @@ export default define(meta, async (ps) => { set.hiddenTags = ps.hiddenTags; } + if (Array.isArray(ps.blockedHosts)) { + set.blockedHosts = ps.blockedHosts; + } + if (ps.mascotImageUrl !== undefined) { set.mascotImageUrl = ps.mascotImageUrl; } diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index e29edae26..1bd88a1e6 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -165,6 +165,7 @@ export default define(meta, async (ps, me) => { response.useStarForReactionFallback = instance.useStarForReactionFallback; response.pinnedUsers = instance.pinnedUsers; response.hiddenTags = instance.hiddenTags; + response.blockedHosts = instance.blockedHosts; response.recaptchaSecretKey = instance.recaptchaSecretKey; response.proxyAccount = instance.proxyAccount; response.twitterConsumerKey = instance.twitterConsumerKey; From c3529f06910c6c50d1f653e8853194f69396aeca Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 14 May 2019 20:53:49 +0900 Subject: [PATCH 008/153] Improve usability --- locales/ja-JP.yml | 1 + src/client/app/admin/views/instance.vue | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 9390af40a..7068e7c3e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1272,6 +1272,7 @@ admin/views/instance.vue: save: "保存" saved: "保存しました" pinned-users: "ピン留めユーザー" + pinned-users-info: "ピン留めしたいユーザーを改行で区切って記述します。" email-config: "メールサーバーの設定" email-config-info: "メールアドレス確認やパスワードリセットの際に使われます。" enable-email: "メール配信を有効にする" diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue index 3bbe6579d..4e7411a72 100644 --- a/src/client/app/admin/views/instance.vue +++ b/src/client/app/admin/views/instance.vue @@ -87,8 +87,10 @@ -
- +
+ + + {{ $t('save') }}
@@ -204,7 +206,7 @@ export default Vue.extend({ enableServiceWorker: false, swPublicKey: null, swPrivateKey: null, - pinnedUsers: [], + pinnedUsers: '', faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt }; }, From 811f9c22d78438a107061b24714539225d44c9c3 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 14 May 2019 20:57:24 +0900 Subject: [PATCH 009/153] :art: --- src/client/app/admin/views/instance.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue index 4e7411a72..5cdd22296 100644 --- a/src/client/app/admin/views/instance.vue +++ b/src/client/app/admin/views/instance.vue @@ -86,7 +86,7 @@ - +
@@ -149,7 +149,7 @@ import Vue from 'vue'; import i18n from '../../i18n'; import { url, host } from '../../config'; import { toUnicode } from 'punycode'; -import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt } from '@fortawesome/free-solid-svg-icons'; +import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt, faThumbtack } from '@fortawesome/free-solid-svg-icons'; import { faEnvelope as farEnvelope } from '@fortawesome/free-regular-svg-icons'; export default Vue.extend({ @@ -207,7 +207,7 @@ export default Vue.extend({ swPublicKey: null, swPrivateKey: null, pinnedUsers: '', - faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt + faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt, faThumbtack }; }, From b779ff08e0aeb95b879972b7dc26bc81a81e627c Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 14 May 2019 21:08:35 +0900 Subject: [PATCH 010/153] =?UTF-8?q?=E7=89=B9=E5=AE=9A=E3=81=AE=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=82=B9=E3=82=BF=E3=83=B3=E3=82=B9=E3=81=AE=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E3=81=99=E3=81=B9=E3=81=A6?= =?UTF-8?q?=E5=89=8A=E9=99=A4=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ja-JP.yml | 1 + src/client/app/admin/views/federation.vue | 19 +++++++++++-- .../admin/federation/delete-all-files.ts | 27 +++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 src/server/api/endpoints/admin/federation/delete-all-files.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7068e7c3e..ed0da44d6 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1441,6 +1441,7 @@ admin/views/federation.vue: latest-request-received-at: "直近のリクエスト受信" remove-all-following: "フォローを全解除" remove-all-following-info: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。" + delete-all-files: "ファイルをすべて削除" block: "ブロック" marked-as-closed: "閉鎖されているとマーク" lookup: "照会" diff --git a/src/client/app/admin/views/federation.vue b/src/client/app/admin/views/federation.vue index 4d0b4b7c7..8b7719069 100644 --- a/src/client/app/admin/views/federation.vue +++ b/src/client/app/admin/views/federation.vue @@ -78,6 +78,10 @@
+
+ {{ $t('delete-all-files') }} + {{ $t('delete-all-files') }} +
{{ $t('remove-all-following') }} {{ $t('remove-all-following') }} @@ -158,7 +162,7 @@ import Vue from 'vue'; import i18n from '../../i18n'; import { faPaperPlane } from '@fortawesome/free-regular-svg-icons'; -import { faBan, faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faTrafficLight, faInbox } from '@fortawesome/free-solid-svg-icons'; +import { faTrashAlt, faBan, faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faTrafficLight, faInbox } from '@fortawesome/free-solid-svg-icons'; import ApexCharts from 'apexcharts'; import * as tinycolor from 'tinycolor2'; @@ -186,7 +190,7 @@ export default Vue.extend({ chartSpan: 'hour', chartInstance: null, blockedHosts: '', - faBan, faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faPaperPlane, faTrafficLight, faInbox + faTrashAlt, faBan, faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faPaperPlane, faTrafficLight, faInbox }; }, @@ -307,6 +311,17 @@ export default Vue.extend({ }); }, + deleteAllFiles() { + this.$root.api('admin/federation/delete-all-files', { + host: this.instance.host + }).then(() => { + this.$root.dialog({ + type: 'success', + splash: true + }); + }); + }, + updateInstance() { this.$root.api('admin/federation/update-instance', { host: this.instance.host, diff --git a/src/server/api/endpoints/admin/federation/delete-all-files.ts b/src/server/api/endpoints/admin/federation/delete-all-files.ts new file mode 100644 index 000000000..befb36226 --- /dev/null +++ b/src/server/api/endpoints/admin/federation/delete-all-files.ts @@ -0,0 +1,27 @@ +import $ from 'cafy'; +import define from '../../../define'; +import del from '../../../../../services/drive/delete-file'; +import { DriveFiles } from '../../../../../models'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + host: { + validator: $.str + } + } +}; + +export default define(meta, async (ps, me) => { + const files = await DriveFiles.find({ + userHost: ps.host + }); + + for (const file of files) { + del(file); + } +}); From 1b58e18a6d61ed73573fba7625dfc0dae22e8009 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 14 May 2019 21:17:29 +0900 Subject: [PATCH 011/153] Update dependencies :rocket: --- package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index bc7b2f514..9a347fb19 100644 --- a/package.json +++ b/package.json @@ -94,13 +94,13 @@ "@types/websocket": "0.0.40", "@types/ws": "6.0.1", "animejs": "3.0.1", - "apexcharts": "3.6.9", + "apexcharts": "3.6.11", "autobind-decorator": "2.4.0", "autosize": "4.0.2", "autwh": "0.1.0", "bcryptjs": "2.4.3", "bootstrap-vue": "2.0.0-rc.13", - "bull": "3.8.1", + "bull": "3.9.1", "cafy": "15.1.1", "chai": "4.2.0", "chalk": "2.4.2", @@ -121,7 +121,7 @@ "feed": "2.0.4", "file-type": "10.11.0", "fuckadblock": "3.2.1", - "gulp": "4.0.1", + "gulp": "4.0.2", "gulp-cssnano": "2.1.3", "gulp-imagemin": "5.0.3", "gulp-mocha": "6.0.0", @@ -140,7 +140,7 @@ "is-root": "2.1.0", "is-svg": "4.1.0", "js-yaml": "3.13.1", - "jsdom": "15.0.0", + "jsdom": "15.1.0", "json5": "2.1.0", "json5-loader": "2.0.0", "katex": "0.10.1", @@ -173,7 +173,7 @@ "os-utils": "0.0.14", "parse5": "5.1.0", "parsimmon": "1.12.0", - "pg": "7.10.0", + "pg": "7.11.0", "portscanner": "2.2.0", "postcss-loader": "3.0.0", "prismjs": "1.16.0", @@ -247,11 +247,11 @@ "vue-template-compiler": "2.6.10", "vuedraggable": "2.20.0", "vuewordcloud": "18.7.11", - "vuex": "3.1.0", + "vuex": "3.1.1", "vuex-persistedstate": "2.5.4", - "web-push": "3.3.3", - "webpack": "4.30.0", - "webpack-cli": "3.3.1", + "web-push": "3.3.4", + "webpack": "4.31.0", + "webpack-cli": "3.3.2", "websocket": "1.0.28", "ws": "7.0.0", "xev": "2.0.1" From e52f9301faf3dc7ec4c8ad95d10b77cbb4c07034 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 14 May 2019 21:18:05 +0900 Subject: [PATCH 012/153] New Crowdin translations (#4878) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Spanish) * New translations ja-JP.yml (Spanish) * New translations ja-JP.yml (Spanish) * New translations ja-JP.yml (Spanish) * New translations ja-JP.yml (Spanish) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Polish) * New translations ja-JP.yml (French) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Czech) * New translations ja-JP.yml (German) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Portuguese) * New translations ja-JP.yml (Spanish) * New translations ja-JP.yml (Japanese, Kansai) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Polish) * New translations ja-JP.yml (French) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Czech) * New translations ja-JP.yml (Dutch) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Japanese, Kansai) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (French) * New translations ja-JP.yml (French) * New translations ja-JP.yml (French) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Polish) * New translations ja-JP.yml (French) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Czech) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Spanish) * New translations ja-JP.yml (Japanese, Kansai) --- locales/cs-CZ.yml | 11 +-- locales/de-DE.yml | 5 -- locales/en-US.yml | 13 +-- locales/es-ES.yml | 151 ++++++++++++++++++++++++++++++++- locales/fr-FR.yml | 53 +++++++++--- locales/ja-KS.yml | 5 +- locales/ko-KR.yml | 206 +++++++++++++++++++++++++++++++++++++++++++--- locales/nl-NL.yml | 2 - locales/pl-PL.yml | 6 +- locales/pt-PT.yml | 7 -- locales/zh-CN.yml | 20 ++--- 11 files changed, 400 insertions(+), 79 deletions(-) diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index c722eabb7..aacbee9de 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -249,7 +249,6 @@ common: update-available-title: "Aktualizace k dispozici" update-available: "Je k dispozici nová verze Misskey ({newer},vaše verze je {current}). Pro aplikování nové verze znovunačtěte stránku." my-token-regenerated: "Váš token byl regenerován, proto budete odhlášen/a." - verified-user: "Ověřené účty" hide-password: "Skrýt heslo" show-password: "Zobrazit heslo" do-not-use-in-production: "Tohle je vývojářský build. Nepoužívejte v produkci." @@ -312,7 +311,6 @@ auth/views/index.vue: error: "Taková relace neexistuje." sign-in: "Prosím přihlaste se." common/views/pages/explore.vue: - verified-users: "Ověřené účty" popular-users: "Populární uživatelé" recently-updated-users: "Nedávno aktívni uživatelé" recently-registered-users: "Nedávno registrovaní uživatelé" @@ -924,7 +922,6 @@ admin/views/instance.vue: invite: "Pozvat" save: "Uložit" saved: "Uloženo" - user-recommendation-config: "Doporučení uživatelé" email: "Emailová adresa" smtp-port: "SMTP Port" smtp-auth: "Provést SMTP autentikaci" @@ -976,12 +973,6 @@ admin/views/users.vue: reset-password: "Resetovat heslo" reset-password-confirm: "Opravdu chcete resetovat Vaše heslo?" password-updated: "Heslo je nyní \"{password}\"" - verify: "Ověřit účet" - verify-confirm: "Chcete aby toto byl ověřený účet?" - verified: "Účet se nyní ověřuje" - unverify: "Zrušit ověření účtu" - unverify-confirm: "Opravdu chcete zrušit designaci \"ověřený účet\"?" - unverified: "Ruší se potvrzení účtu" update-remote-user: "Aktualizovat informace o vzdáleném účtu" users: title: "Uživatel" @@ -989,7 +980,6 @@ admin/views/users.vue: all: "Všechny" moderator: "Moderátor" adminOrModerator: "Admin/Moderátor" - verified: "Ověřený účet" origin: title: "Původ" combined: "Lokální + Vzdálené" @@ -1054,6 +1044,7 @@ admin/views/federation.vue: chart-spans: hour: "za hodinu" day: "za den" + blocked-hosts: "Blokován" desktop/views/pages/welcome.vue: about: "O Misskey" timeline: "Časová osa" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 53a3fc79f..b615a893a 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -153,7 +153,6 @@ common: update-available-title: "Aktualisierung verfügbar" update-available: "Eine neue Version von Misskey ist verfügbar ({newer}, aktuell ist {current}). Lade die Seite neu um die aktuelle Version zu laden" my-token-regenerated: "Dein Token wurde generiert. Du wirst jetzt abgemeldet." - verified-user: "Verifizierter Benutzer" do-not-use-in-production: "Dies ist eine Entwicklungsversion. Nicht in einer Produktivumgebung verwenden." error: retry: "Erneut versuchen" @@ -199,8 +198,6 @@ auth/views/index.vue: please-go-back: "Bitte gehe zurück zur Anwendung." error: "Sitzung ist nicht vorhanden." sign-in: "Bitte melde dich an." -common/views/pages/explore.vue: - verified-users: "Verifizierter Benutzer" common/views/components/games/reversi/reversi.vue: matching: waiting-for: "Warten auf {}" @@ -605,8 +602,6 @@ admin/views/drive.vue: delete: "Löschen" admin/views/users.vue: users: - state: - verified: "Verifizierter Benutzer" origin: local: "Lokal" admin/views/emoji.vue: diff --git a/locales/en-US.yml b/locales/en-US.yml index 4293b81d0..f32359306 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -251,7 +251,6 @@ common: update-available-title: "Update available" update-available: "A new version of Misskey is now available({newer}, the current version is {current}). Reload the page to apply updates." my-token-regenerated: "Your token has been regenerated, so you will be signed out." - verified-user: "Verified account" hide-password: "Hide Password" show-password: "Show Password" do-not-use-in-production: "This is a development build. Do not use in production." @@ -319,7 +318,7 @@ auth/views/index.vue: error: "Session does not exist." sign-in: "Please sign in." common/views/pages/explore.vue: - verified-users: "Official accounts" + pinned-users: "Higlighted users" popular-users: "Popular users" recently-updated-users: "Recently active users" recently-registered-users: "Users who joined recently" @@ -1139,7 +1138,7 @@ admin/views/instance.vue: invite: "Invite" save: "Save" saved: "Saved" - user-recommendation-config: "Recommended users" + pinned-users: "Higlighted user" email-config: "Email server settings" email-config-info: "Used to confirm email and password reset etc." enable-email: "Enable email delivery" @@ -1223,12 +1222,6 @@ admin/views/users.vue: silence-confirm: "Silence user?" unmake-silence: "Unsilence" unsilence-confirm: "Are you certain that you want to stop silencing this user?" - verify: "Verify account" - verify-confirm: "Do you want this to be a verified account?" - verified: "The account is now being verified" - unverify: "Unverify account" - unverify-confirm: "Do you want to remove the 'verified account' designation?" - unverified: "The account is now being unverified" update-remote-user: "Update information about remote user" remote-user-updated: "The information regarding the remote user has been updated." users: @@ -1245,7 +1238,6 @@ admin/views/users.vue: admin: "Administrator" moderator: "Moderator" adminOrModerator: "Admin/Moderator" - verified: "Verified account" silenced: "Already silenced" suspended: "Suspended" origin: @@ -1353,6 +1345,7 @@ admin/views/federation.vue: chart-spans: hour: "Hourly" day: "Daily" + blocked-hosts: "Blocking" desktop/views/pages/welcome.vue: about: "More details..." timeline: "Timeline" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 5d7d1214f..2cdc36fbe 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -12,7 +12,9 @@ common: rich-contents: "Posts" rich-contents-desc: "Escribe sobre tus pensamientos, eventos, todo lo que quieras compartir. Si es necesario, puedes usar varias sintaxis, decorar tus posts y añadir tus imágenes favoritas, archivos de viddeo y encuestas." reaction: "Reacciones" + reaction-desc: "La forma mas facil de expresar tus emociones. Misskey te permite añadir varios tipos de reacciones a los posts de otros usuarios. La emperiencia emocional en Misskey nunca será igual que en otra red social, donde solo puedes poner \"likes\"." ui: "Interfaz" + ui-desc: "No hay ninguna interfaz que le vaya bien a todos. Por eso, Misskey tiene una interfaz altamente personalizable para tus gustos. Puedes hacer tu página principal única editando la interfaz de tu timeline y moviendo varios widgets para conseguir hacer de este lugar uno propio." drive: "Drive" adblock: detected: "Por favor, desactive el bloqueador de publicidad." @@ -55,6 +57,7 @@ common: month-and-day: "{day} de {month}" trash: "Papelera" drive: "Drive" + pages: "Páginas" messaging: "Conversación" home: "Inicio" deck: "Deck" @@ -70,8 +73,12 @@ common: "write:blocks": "Editar bloques" "read:favorites": "Ver favoritos" "write:favorites": "Editar favoritos" + "read:following": "Ver información de seguidor" "read:messaging": "Ver conversación" + "read:mutes": "Ver silenciados" + "write:notes": "Crear y eliminar articulos" "read:notifications": "Ver notificaciones" + "read:reactions": "Ver reacciones" "write:votes": "Vota" weekday-short: sunday: "domingo" @@ -136,8 +143,11 @@ common: default-note-visibility: "Rango de publicación predeterminado" web-search-engine: "Buscador web" web-search-engine-desc: "Ejemplo: https://www.google.com/?#q={{query}}" + this-setting-is-this-device-only: "Solo para este dispositivo" use-os-default-emojis: "Usar los emoticonos estándar del sistema operativo" line-width: "Grosor de línea" + line-width-thin: "Fino" + line-width-normal: "Normal" line-width-thick: "Grosor" font-size: "Tamaño del texto" font-size-x-small: "Muy pequeño" @@ -162,13 +172,20 @@ common: wallpaper: "Fondo de pantalla" choose-wallpaper: "Escoge un fondo de pantalla" delete-wallpaper: "Quitar fondo de pantalla" + post-form-on-timeline: "Mostrar el formulario de las entradas encima de la línea de tiempo" show-clock-on-header: "Muestra el reloj en la parte superior derecha" + show-reply-target: "Mostrar destinatario de la mención" timeline: "Timeline" + show-my-renotes: "Mostrar mis renotes en la timeline" + show-renoted-my-notes: "Mostrar renotes de mis posts en la timeline" sound: "Sonido" enable-sounds: "Habilitar sonido" volume: "Volúmen" test: "Prueba" + update: "Actualizar Misskey" version: "Versión" + latest-version: "Última versión" + update-checking: "Buscando actualizaciones" no-updates: "No hay actualizaciones disponibles" no-updates-desc: "Tu Misskey está actualizado" update-available: "¡Una nueva versión está disponible!" @@ -178,18 +195,36 @@ common: search: "Buscar" delete: "eliminar" loading: "cargando" + ok: "Confirmar" + cancel: "Cancelar" update-available-title: "Actualización disponible" update-available: "Hay disponible una nueva versión de Misskey ({newer}, la versión actual es {current}). Refresca la página para aplicar las actualizaciones." my-token-regenerated: "Tu token se ha regenerado vas a ser desconectado." - verified-user: "Cuenta verificada" + hide-password: "Ocultar contraseña" + show-password: "Mostrar contraseña" do-not-use-in-production: "Esto está en desarrollo, no usarlo para producción." + user-suspended: "Este usuario ha sido suspendido" + is-remote-user: "La información sobre este usuario puede no estar completa" + is-remote-post: "Es una publicación remota" + view-on-remote: "Consultar el perfil completo" + renoted-by: "Renotado por {user}" + no-notes: "No hay publicaciones" + turn-on-darkmode: "Cambiar a modo oscuro" + turn-off-darkmode: "Modo claro" + error: + title: "Se ha producido un problema :(" + retry: "Inténtalo otra vez" reversi: drawn: "Empatado" my-turn: "Mi turno" opponent-turn: "Turno del oponente" + turn-of: "Turno de {name}" + past-turn-of: "Turno de {name}" + won: "{name} ha ganado" black: "Negro" white: "Blanco" total: "Total" + this-turn: "Turno {count}" widgets: analog-clock: "Reloj analógico" profile: "Perfil" @@ -212,8 +247,12 @@ common: nav: "Navegación" tips: "Consejos" hashtags: "Etiquetas" + queue: "En cola" + dev: "Se ha producido un error creando la aplicación. Intentelo de nuevo." + ai-chan-kawaii: "Ai-chan es muy mona!" you: "Tú" auth/views/form.vue: + share-access: "¿Deseas permitir a {name} acceder a tu cuenta?" permission-ask: "La aplicación requiere los siguientes permisos:" cancel: "Cancelar" accept: "Garantizar acceso." @@ -228,7 +267,18 @@ auth/views/index.vue: error: "Esta sesión no existe." sign-in: "Por favor inicia sesión." common/views/pages/explore.vue: - verified-users: "Cuenta verificada" + popular-users: "Usuarios populares" + recently-updated-users: "Usuarios activos recientemente" + recently-registered-users: "Usuarios que se han unido recientemente" + popular-tags: "Etiquetas populares" + federated: "Desde el fediverso" + explore: "Explorar {host}" + users-info: "Actualmente hay {users} registrados aquí" +common/views/components/url-preview.vue: + enable-player: "Activar reproducción" + disable-player: "Cerrar el reproductor" +common/views/components/user-list.vue: + no-users: "No hay usuarios." common/views/components/games/reversi/reversi.vue: matching: waiting-for: "Esperando por {}" @@ -244,6 +294,7 @@ common/views/components/games/reversi/reversi.index.vue: sub-title: "¡Juega Reversi con tus amigos!" invite: "Invitar" rule: "Cómo jugar" + rule-desc: "Reversi es un juego de estrategia para dos jugadores, el cual se juega en un tablero de 8x8. Hay 64 fichas llamadas discos, las cuales son claras de un lado y oscuras del otro. Los jugadores toman turnos colocando fichas en el tablero con su color asignado mirando hacia arriba. Durante una jugada, cualquier disco del color del oponente que esté en fila entre un disco del oponente y otro del mismo color, será volteado para tener el color del jugador que haya hecho la movida. El objetivo del juego es tener la mayoría de los discos de tu color cuando el último cuadro es llenado." mode-invite: "Invitar" mode-invite-desc: "Invitar un usuario al juego." invitations: "¡Has recibido una invitación!" @@ -300,23 +351,40 @@ common/views/components/media-banner.vue: click-to-show: "Click para mostrar" common/views/components/theme.vue: theme: "Tema" + light-theme: "Tema a usar en Light mode" + dark-theme: "Tema a usar en dark mode" light-themes: "Tema claro" dark-themes: "Tema oscuro" install-a-theme: "Instalar tema" theme-code: "Código del tema" install: "Instalación" + installed: "\"{}\" se ha instalado" + create-a-theme: "Crear tema" + save-created-theme: "Guardar tema" primary-color: "Color primario" secondary-color: "Color secundario" + text-color: "Color del texto" base-theme: "Tema base" base-theme-light: "Claro" base-theme-dark: "Oscuro" + find-more-theme: "Obtener más temas" theme-name: "Nombre del tema" preview-created-theme: "Vista previa" + invalid-theme: "No es un tema válido" + already-installed: "Este tema ya está instalado." + saved: "Guardado" + manage-themes: "Gestor de temas" + builtin-themes: "Temas estandar" my-themes: "Mis temas" installed-themes: "Temas instalados" + select-theme: "Elegir tema" uninstall: "Desinstalar" + uninstalled: "\"{}\" ha sido desinstalado" + author: "Autor" + desc: "Descripción" export: "Exportar" import: "Importar" + import-by-code: "o pega el código" common/views/components/cw-button.vue: show: "Mostrar" chars: "{count} letras" @@ -430,10 +498,25 @@ common/views/components/stream-indicator.vue: connected: "Conectado" common/views/components/notification-settings.vue: title: "Notificaciones" +common/views/components/integration-settings.vue: + title: "Integraciones" + connect: "Conectar" + disconnect: "Desconectarse" + connected-to: "Estas conectado a la siguiente cuenta" common/views/components/github-setting.vue: + description: "Una vez conectada tu cuenta de GitHub a Misskey podrás ver la información sobre tu perfil de GitHub y además podrás registrarte mediante tu cuenta de GitHub." + connected-to: "Estas conectado a esta cuenta de GitHub" detail: "Ver detalles..." + reconnect: "Reconectar" + connect: "Vincular tu cuenta de GitHub" + disconnect: "Desconectarse" common/views/components/discord-setting.vue: + description: "Una vez conectada tu cuenta de Discord a Misskey podrás ver la información sobre tu perfil de Discord y además podrás registrarte mediante tu cuenta de Discord." + connected-to: "Estas conectado a esta cuenta de Discord" detail: "Ver detalles..." + reconnect: "Reconectar" + connect: "Vincular tu cuenta de Discord" + disconnect: "Desconectarse" common/views/components/uploader.vue: waiting: "Un momento" common/views/components/visibility-chooser.vue: @@ -445,27 +528,65 @@ common/views/components/visibility-chooser.vue: specified: "Directo" specified-desc: "Publica solo para los seguidores que quieras" local-public: "Público (sólo local)" + local-public-desc: "No publicar para remoto" local-home: "Inicio (sólo local)" local-followers: "Seguidores (sólo local)" +common/views/components/trends.vue: + count: "{} usuarios mencionados" + empty: "Ninguna tendencia popular ahora" +common/views/components/language-settings.vue: + title: "Mostrar idioma" + pick-language: "Selecciona un idioma" + recommended: "Recomendado" + auto: "Automático" + specify-language: "Especifica el idioma" + info: "Necesitas recargar la página para que los cambios tengan efecto." common/views/components/profile-editor.vue: title: "Perfil" name: "Nombre" account: "Cuenta" + location: "Localización" + description: "Acerca de mí" + you-can-include-hashtags: "También puedes incluir hashtags en la descripción de tu perfil." + language: "Idioma" + birthday: "Fecha de nacimiento" avatar: "Avatar" banner: "Banner" is-cat: "Esta cuenta es un gato" is-bot: "Esta cuenta es un bot" + is-locked: "Las peticiones de seguimiento necesitan aprobación" + careful-bot: "Las peticiones de seguimiento de bots necesitan aprobación" + auto-accept-followed: "Aprobar automaticamente las peticiones de follow de gente a la que sigues" + advanced: "Otros" + privacy: "Privacidad" save: "Guardar" + saved: "Perfil actualizado con exito" + uploading: "Subiendo" + upload-failed: "Error al subir" + email: "Preferencias de correo" email-address: "Correo electrónico" + email-verified: "Tu cuenta de correo ha sido verificada." + email-not-verified: "Tu cuenta de correo no está verificada. Por favor comprueba tu bandeja de entrada." export: "Exportar" import: "Importar" + export-and-import: "Exportar/Importar" export-targets: + all-notes: "Todas las notas publicadas" + following-list: "Seguidores" mute-list: "Silenciar" blocking-list: "Bloquear" user-lists: "Listas" + export-requested: "Has solicitado una exportación. Esto puede tardar un rato. Después de que termine la exportación el archivo se añadirá al drive." + import-requested: "Has empezado una importación. Esto puede tardar un rato." enter-password: "Escribe una contraseña" + danger-zone: "Zona de peligro" + delete-account: "Eliminar cuenta" + account-deleted: "Esta cuenta ha sido eliminada. Puede tardar un rato hasta que toda la información desaparazca." common/views/components/user-list-editor.vue: users: "Usuarios" + rename: "Cambiar el nombre de la lista" + delete: "Eliminar lista" + remove-user: "Eliminar de la lista" common/views/components/user-lists.vue: list-name: "Nombre de lista" common/views/widgets/broadcast.vue: @@ -788,24 +909,46 @@ admin/views/index.vue: instance: "Instancia" moderators: "Moderadores" users: "Usuarios" + federation: "Federado" hashtags: "Hashtags" + queue: "Cola de trabajos" + logs: "Registros" back-to-misskey: "Volver a Misskey" admin/views/dashboard.vue: dashboard: "Panel de Control" accounts: "Cuenta" + notes: "Publicaciones" drive: "Drive" instances: "Instancias" this-instance: "Esta instancia" + federated: "Federado" +admin/views/queue.vue: + title: "Cola" + remove-all-jobs: "Limpiar todos los trabajos pendientes" admin/views/abuse.vue: + title: "Abuso" + target: "Destinatario" + reporter: "Informador" details: "Detalles" remove-report: "eliminar" admin/views/instance.vue: instance: "Instancia" instance-name: "Nombre de la instancia" + instance-description: "Descripción de la instancia" host: "Host" + banner-url: "URL de la imagen de banner" + error-image-url: "Error en la URL de la imagen" + languages: "Idioma de esta instancia" + languages-desc: "Puedes añadir mas de uno, separado por espacios." + maintainer-config: "Información del administrador" + maintainer-name: "Nombre del administrador" + maintainer-email: "Contactar con el administrador" + drive-config: "Ajustes del Drive" + cache-remote-files: "Mantener en cache los archivos remotos" recaptcha-secret-key: "clave secreta reCAPTCHA" invite: "Invitar" save: "Guardar" + saved: "Guardado" email: "Correo electrónico" smtp-host: "Host SMTP" smtp-port: "Puerto SMTP" @@ -834,7 +977,6 @@ admin/views/users.vue: state: all: "Todo" moderator: "Moderadores" - verified: "Cuenta verificada" origin: local: "Local" admin/views/emoji.vue: @@ -846,12 +988,14 @@ admin/views/announcements.vue: save: "Guardar" remove: "eliminar" add: "Agregar" + saved: "Guardado" admin/views/federation.vue: instance: "Instancia" host: "Host" following: "Siguiendo" status: "Estado" block: "Bloquear" + instances: "Federado" states: all: "Todo" blocked: "Bloquear" @@ -859,6 +1003,7 @@ admin/views/federation.vue: chart-spans: hour: "Por hora" day: "Por día" + blocked-hosts: "Bloquear" desktop/views/pages/selectdrive.vue: cancel: "Cancelar" desktop/views/pages/user-list.users.vue: diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index ae414d1ce..eab827a77 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -72,12 +72,18 @@ common: permissions: "read:account": "Afficher les informations du compte" "write:account": "Mettre à jour les informations de votre compte" + "read:blocks": "Voir les blocs" + "write:blocks": "Écrire des blocs" "read:drive": "Parcourir le Drive" "write:drive": "Écrire sur le Drive" "read:favorites": "Afficher les favoris" + "write:favorites": "Écrire des favoris" + "write:messaging": "Utiliser la messagerie" "write:notes": "Créer ou supprimer des publications" "read:notifications": "Afficher les notifications" + "write:notifications": "Gérer vos notifications" "read:reactions": "Lire les réactions" + "write:reactions": "Gérer vos réactions" "write:votes": "Vote" empty-timeline-info: follow-users-to-make-your-timeline: "Les utilisateurs suivants afficheront leurs publications sur votre fil." @@ -133,7 +139,7 @@ common: notification: "Notifications" apps: "Applications" tags: "Hashtags" - mute-and-block: "Silencer / Bloquer" + mute-and-block: "Silencés / Bloqués" blocking: "En cours blocage" security: "Sécurité" signin: "Historique des connexions" @@ -193,6 +199,7 @@ common: show-clock-on-header: "Afficher l'horloge sur le coté supérieur droit" timeline: "Fil d’actualité" show-my-renotes: "Afficher mes republications dans le fil" + show-renoted-my-notes: "Afficher les partages de mes propres notes sur le fil" remain-deleted-note: "Continuer à afficher les notes supprimées" sound: "Son" enable-sounds: "Activer les sons" @@ -234,7 +241,6 @@ common: update-available-title: "Mise à jour disponible" update-available: "Une nouvelle version de Misskey est disponible ({newer}, version actuelle: {current}). Veuillez recharger la page pour appliquer la mise à jour." my-token-regenerated: "Votre jeton vient d’être généré, vous allez maintenant être déconnecté." - verified-user: "Compte vérifié" hide-password: "Masquer le mot de passe" show-password: "Afficher le mot de passe" do-not-use-in-production: "Il s’agit d’une version de développement. Ne pas utiliser dans un environnement de production." @@ -302,7 +308,6 @@ auth/views/index.vue: error: "La session n’existe pas." sign-in: "Veuillez vous connecter" common/views/pages/explore.vue: - verified-users: "Comptes vérifiés" popular-users: "Utilisateurs populaires" recently-updated-users: "Utilisateurs actifs récemment" recently-registered-users: "Les nouveaux inscrits" @@ -455,6 +460,7 @@ common/views/components/nav.vue: repository: "Dépôt" develop: "Développeurs" feedback: "Suggestions" + tos: "Conditions d'utilisation" common/views/components/note-menu.vue: mention: "Mention" detail: "Détails" @@ -473,8 +479,12 @@ common/views/components/user-menu.vue: mention: "Mention" mute: "Silencier" unmute: "Enlever la sourdine" + mute-confirm: "Rendre muet cet utilisateur ?" + unmute-confirm: "Ne plus masquer cet utilisateur ?" block: "Bloquer" unblock: "Débloquer" + block-confirm: "Bloquer cet utilisateur ?" + unblock-confirm: "Débloquer cet utilisateur ?" push-to-list: "Ajouter à une liste" select-list: "Sélectionnez une liste" report-abuse: "Signaler un abus" @@ -557,6 +567,7 @@ common/views/components/signup.vue: password-matched: "OK" password-not-matched: "Les mots de passe ne correspondent pas." recaptcha: "Vérifier" + tos: "Conditions d'utilisation" create: "Créer un compte" some-error: "La création du compte a échoué. Veuillez réessayer." common/views/components/special-message.vue: @@ -1106,7 +1117,6 @@ admin/views/instance.vue: invite: "Inviter" save: "Sauvegarder" saved: "Enregistré" - user-recommendation-config: "Utilisateurs" email-config: "Paramètres du serveur de messagerie" email-config-info: "Utilisé pour confirmer votre adresse de courrier électronique et la réinitialisation de votre mot de passe." enable-email: "Activation de la distribution du courrier" @@ -1187,13 +1197,8 @@ admin/views/users.vue: unsuspend-confirm: "Souhaiteriez-vous ne plus suspendre ce compte ?" unsuspended: "La suspension de l’utilisateur a été levée avec succès" make-silence: "Mettre en sourdine" + silence-confirm: "Mettre l'utilisateur sous silence ?" unmake-silence: "Enlever la sourdine" - verify: "Vérification du compte" - verify-confirm: "Souhaiteriez-vous rendre votre compte comme étant un compte vérifié ?" - verified: "Le compte a été vérifié" - unverify: "Enlever la vérification du compte" - unverify-confirm: "Désirez-vous considérer ce compte comme étant non-vérifié ?" - unverified: "Ce compte n'est plus vérifié" update-remote-user: "Mettre à jour les informations de l’utilisateur·rice distant·e" remote-user-updated: "Les informations de l’utilisateur·rice distant·e ont étés mis à jour" users: @@ -1210,7 +1215,6 @@ admin/views/users.vue: admin: "Admin" moderator: "Modérateur" adminOrModerator: "Administrateur/Modérateur" - verified: "Compte vérifié" silenced: "Déjà mis en sourdine" suspended: "Suspendu" origin: @@ -1309,6 +1313,7 @@ admin/views/federation.vue: chart-spans: hour: "Par heure" day: "Par jour" + blocked-hosts: "En cours blocage" desktop/views/pages/welcome.vue: about: "à propos" timeline: "Fil d’actualité" @@ -1589,7 +1594,30 @@ dev/views/new-app.vue: authority-desc: "Sont accessibles via l’API, uniquement les fonctionnalités demandées ici." authority-warning: "Vous pouvez le changer même après avoir créé l'application, mais si vous attribuez une nouvelle permission, toutes les clés utilisateur associées seront dès lors invalides." pages: + page-created: "Page a été créée !" + are-you-sure-delete: "Confirmez-vous la suppression de cette page ?" + page-deleted: "La page a bien été supprimée." + edit-this-page: "Éditer cette page" + view-source: "Afficher la source" + view-page: "Afficher la page" + inspector: "Inspecteur" + content: "Bloc de page" + variables: "Variables" + more-details: "Description" title: "Titre" + url: "URL de page" + summary: "Résumé de page" + align-center: "Centrée" + font: "Police de caractères" + fontSerif: "Serif" + fontSansSerif: "Sans Serif" + choose-block: "Ajouter un bloc" + select-type: "Choisir un type" + enter-variable-name: "Veuillez choisir un nom de variable" + the-variable-name-is-already-used: "Cette variable est déjà utilisée" + content-blocks: "Contenu du cadre" + special-blocks: "Spécial" + posted-from-post-form: "Publié !" blocks: text: "Texte" textarea: "Zone de texte" @@ -1602,6 +1630,7 @@ pages: post: "Champs de publication" _post: text: "Contenu" + textInput: "Entrée textuelle" _textInput: name: "Nom de la variable" text: "Titre" @@ -1610,6 +1639,7 @@ pages: name: "Nom de la variable" text: "Titre" default: "Valeur par défaut" + numberInput: "Entrée numérique" _numberInput: name: "Nom de la variable" text: "Titre" @@ -1717,6 +1747,7 @@ pages: arg1: "Numérique" _splitStrByLine: arg1: "Texte" + ref: "Variables" fn: "Fonction" _fn: arg1: "Sortie" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index f3ea9bc4f..224f0a40a 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -121,7 +121,6 @@ common: update-available-title: "更新があんで" update-available: "Misskeyの新しいバージョンがあんで({newer}。現在{current}をつこてるわ)。ページを再度読み込みしたると更新が適用されるわ。" my-token-regenerated: "あんさんのトークンが更新されたらしいわ。すまんがとりあえずサインアウトすんで。" - verified-user: "アメちゃん付きアカウント" do-not-use-in-production: "開発ビルドや。本番環境で使わんといて!知らんで!" is-remote-post: "この投稿情報はコピーです。" view-on-remote: "ちゃんとした情報見せてや!" @@ -181,7 +180,6 @@ auth/views/index.vue: error: "セッションが存在してへん。" sign-in: "サインインしてや" common/views/pages/explore.vue: - verified-users: "アメちゃん付きアカウント" federated: "連合" common/views/components/games/reversi/reversi.vue: matching: @@ -899,7 +897,6 @@ admin/views/instance.vue: invite: "来てや" save: "保存" saved: "保存したで!" - user-recommendation-config: "このユーザーええで" email-config: "メールサーバーの設定" email-config-info: "メールアドレス確認やパスワードリセットの際に使うで。" enable-email: "メール配信を有効にする" @@ -956,7 +953,6 @@ admin/views/users.vue: state: all: "すべて" moderator: "モデレーター" - verified: "アメちゃん付きアカウント" origin: local: "ローカル" admin/views/emoji.vue: @@ -995,6 +991,7 @@ admin/views/federation.vue: chart-spans: hour: "1時間ごと" day: "1日ごと" + blocked-hosts: "ブロック" desktop/views/pages/welcome.vue: about: "もうちょい……" timeline: "タイムライン" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index aec632d84..4434cf881 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -251,7 +251,6 @@ common: update-available-title: "업데이트가 있습니다" update-available: "Misskey의 새로운 버전이 있습니다 ({newer}. 현재 {current}을 사용 중). 페이지를 다시 로드하면 업데이트가 적용됩니다." my-token-regenerated: "당신의 토큰이 업데이트되었으므로 로그아웃합니다." - verified-user: "공식 계정" hide-password: "비밀번호 숨기기" show-password: "비밀번호 표시" do-not-use-in-production: "이것은 개발 빌드입니다. 프로덕션 환경에서 사용하지 마십시오." @@ -294,7 +293,7 @@ common: notifications: "알림" users: "추천 사용자" polls: "투표" - post-form: "게시 양식" + post-form: "글 입력란" server: "서버 정보" nav: "내비게이션" tips: "팁" @@ -319,7 +318,7 @@ auth/views/index.vue: error: "세션이 존재하지 않습니다." sign-in: "로그인 해주시기 바랍니다" common/views/pages/explore.vue: - verified-users: "공식 계정" + pinned-users: "고정된 사용자" popular-users: "인기 사용자" recently-updated-users: "최근 게시한 사용자" recently-registered-users: "신규 사용자" @@ -473,6 +472,7 @@ common/views/components/nav.vue: repository: "저장소" develop: "개발자" feedback: "피드백" + tos: "이용 약관" common/views/components/note-menu.vue: mention: "멘션" detail: "상세" @@ -585,6 +585,7 @@ common/views/components/signup.vue: password-matched: "확인되었습니다" password-not-matched: "일치하지 않습니다" recaptcha: "자동 가입 방지" + tos: "이용 약관" create: "계정 만들기" some-error: "알 수 없는 이유로 계정 만들기에 실패했습니다. 다시 한번 시도해 주세요." common/views/components/special-message.vue: @@ -1091,10 +1092,15 @@ admin/views/instance.vue: instance-name: "인스턴스 이름" instance-description: "인스턴스의 소개" host: "관리자" + icon-url: "아이콘 URL" + logo-url: "로고 URL" banner-url: "배너 이미지 URL" error-image-url: "오류 이미지 URL" languages: "인스턴스의 대상 언어" languages-desc: "공백으로 구분하여 여러 개 설정할 수 있습니다." + tos-url: "이용약관 URL" + repository-url: "저장소 URL" + feedback-url: "피드백 URL" maintainer-config: "관리자 정보" maintainer-name: "관리자 이름" maintainer-email: "관리자 연락처" @@ -1139,7 +1145,7 @@ admin/views/instance.vue: invite: "초대" save: "저장" saved: "저장하였습니다" - user-recommendation-config: "추천 사용자" + pinned-users: "고정된 사용자" email-config: "메일 서버 설정" email-config-info: "메일 주소 확인 혹은 비밀번호 재설정에 사용 됩니다." enable-email: "메일 발신 활성화" @@ -1223,12 +1229,6 @@ admin/views/users.vue: silence-confirm: "침묵으로 설정합니까?" unmake-silence: "침묵 해제" unsilence-confirm: "침묵 해제하시겠습니까?" - verify: "공식 계정으로 설정" - verify-confirm: "공식 계정으로 설정하시겠습니까?" - verified: "공식 계정으로 설정하였습니다" - unverify: "공식 계정 해제" - unverify-confirm: "공식 계정을 해제하시겠습니까?" - unverified: "공식 계정을 해제하였습니다" update-remote-user: "원격 사용자 정보 갱신" remote-user-updated: "원격 사용자 정보를 갱신하였습니다" users: @@ -1245,7 +1245,6 @@ admin/views/users.vue: admin: "관리자" moderator: "모더레이터" adminOrModerator: "관리자+모더레이터" - verified: "공식 계정" silenced: "침묵됨" suspended: "정지됨" origin: @@ -1353,6 +1352,7 @@ admin/views/federation.vue: chart-spans: hour: "1시간마다" day: "1일마다" + blocked-hosts: "차단" desktop/views/pages/welcome.vue: about: "자세히..." timeline: "타임라인" @@ -1652,42 +1652,224 @@ pages: page-created: "페이지를 만들었습니다" page-updated: "페이지를 수정했습니다" are-you-sure-delete: "이 페이지를 삭제하시겠습니까?" + page-deleted: "페이지가 삭제되었습니다" + edit-this-page: "이 페이지를 편집" + view-source: "소스 보기" + view-page: "페이지 보기" + inspector: "인스펙터" + content: "페이지 블록" + variables: "변수" + more-details: "자세한 설명" title: "제목" + url: "페이지 URL" + summary: "페이지 요약" + align-center: "가운데 정렬" + font: "글꼴" + fontSerif: "세리프" + fontSansSerif: "산 세리프" + set-eye-catching-image: "아이캐치 이미지를 설정" + remove-eye-catching-image: "아이캐치 이미지를 삭제" + choose-block: "블록 추가" + select-type: "종류 선택" + enter-variable-name: "변수명을 설정해주십시오" + the-variable-name-is-already-used: "그 변수명은 이미 사용중입니다" + content-blocks: "콘텐츠" + input-blocks: "입력" + special-blocks: "특수" + post-from-post-form: "이 내용을 올리기" + posted-from-post-form: "게시하였습니다" blocks: + text: "텍스트" + textarea: "텍스트 영역" + section: "섹션" image: "이미지" - post: "게시 양식" + button: "버튼" + if: "만약" + _if: + variable: "변수" + post: "글 입력란" _post: text: "내용" + textInput: "텍스트 입력" _textInput: + name: "변수명" text: "제목" + default: "기본값" + textareaInput: "여러 줄 텍스트 입력" _textareaInput: + name: "변수명" text: "제목" + default: "기본값" + numberInput: "수치 입력" _numberInput: + name: "변수명" text: "제목" + default: "기본값" + switch: "스위치" _switch: + name: "변수명" text: "제목" + default: "기본값" + counter: "카운터" _counter: + name: "변수명" text: "제목" + inc: "증가치" _button: text: "제목" + action: "버튼을 눌렀을 때의 동작" _action: + dialog: "대화상자를 표시" _dialog: content: "내용" + resetRandom: "난수를 초기화" script: categories: + flow: "흐름 제어" + logical: "논리 연산" + operation: "계산" + comparison: "비교" random: "랜덤" + value: "값" + fn: "함수" + text: "텍스트 조작" + convert: "변환" list: "리스트" blocks: + text: "텍스트" + multiLineText: "텍스트 (여러줄)" + textList: "텍스트 목록" + strLen: "텍스트의 길이" + _strLen: + arg1: "텍스트" + strPick: "문자 추출" + _strPick: + arg1: "텍스트" + arg2: "문자 위치" + strReplace: "텍스트 치환" + _strReplace: + arg1: "텍스트" + arg2: "치환 전" + arg3: "치환 후" + strReverse: "텍스트 뒤집기" + _strReverse: + arg1: "텍스트" + join: "텍스트 접합" _join: arg1: "리스트" + arg2: "구분자" + add: "+ 더하기" + _add: + arg1: "A" + arg2: "B" + subtract: "- 빼기" + _subtract: + arg1: "A" + arg2: "B" + multiply: "× 곱하기" + _multiply: + arg1: "A" + arg2: "B" + divide: "÷ 나누기" + _divide: + arg1: "A" + arg2: "B" + remind: "÷ 나눈 나머지" + _remind: + arg1: "A" + arg2: "B" + eq: "A와 B가 동일" + _eq: + arg1: "A" + arg2: "B" + notEq: "A와 B가 다름" + _notEq: + arg1: "A" + arg2: "B" + and: "A 그리고 B" + _and: + arg1: "A" + arg2: "B" + or: "A 혹은 B" + _or: + arg1: "A" + arg2: "B" + lt: "< A가 B보다 작음" + _lt: + arg1: "A" + arg2: "B" + gt: "> A가 B보다 큼" + _gt: + arg1: "A" + arg2: "B" + ltEq: "<= A가 B보다 작거나 같음" + _ltEq: + arg1: "A" + arg2: "B" + gtEq: ">= A가 B보다 크거나 같음" + _gtEq: + arg1: "A" + arg2: "B" + if: "분기" + _if: + arg1: "만약" + arg2: "그러면" + arg3: "그렇지 않으면" + not: "부정" + _not: + arg1: "부정" random: "랜덤" + _random: + arg1: "확률" + rannum: "난수" + _rannum: + arg1: "최소" + arg2: "최대" + randomPick: "목록에서 임의로 선택" _randomPick: arg1: "리스트" + _dailyRandom: + arg1: "확률" + _dailyRannum: + arg1: "최소" + arg2: "최대" _dailyRandomPick: arg1: "리스트" + _seedRandom: + arg1: "시드" + arg2: "확률" + _seedRannum: + arg1: "시드" + arg2: "최소" + arg3: "최대" _seedRandomPick: + arg1: "시드" arg2: "리스트" + _DRPWPM: + arg1: "텍스트 목록" _pick: arg1: "리스트" + number: "수치" + stringToNumber: "텍스트를 수치로" + _stringToNumber: + arg1: "텍스트" + _numberToString: + arg1: "수치" + _splitStrByLine: + arg1: "텍스트" + ref: "변수" + fn: "함수" + _fn: + slots: "슬롯" + arg1: "출력" + thereIsEmptySlot: "슬롯 {slot}이(가) 비었습니다!" types: + string: "텍스트" + number: "수치" + boolean: "플래그" array: "리스트" + stringArray: "텍스트 목록" + emptySlot: "빈 슬롯" + enviromentVariables: "환경 변수" + pageVariables: "페이지 요소" + argVariables: "입력 슬롯" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 755f8ab3f..cfff13f71 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -422,8 +422,6 @@ admin/views/dashboard.vue: notes: "Bericht" admin/views/abuse.vue: remove-report: "Verwijderen" -admin/views/instance.vue: - user-recommendation-config: "Aanbevolen gebruikers" admin/views/charts.vue: notes: "Bericht" users: "Gebruiker" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 49845177f..5d61fb5f6 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -166,7 +166,6 @@ common: update-available-title: "Aktualizacja jest dostępna" update-available: "Nowa wersja Misskey jest dostępna ({newer}, obecna to {current}). Odśwież stronę, aby zastosować aktualizację." my-token-regenerated: "Twój token został wygenerowany. Zostaniesz wylogowany." - verified-user: "Zweryfikowane konto" hide-password: "Ukryj hasło" show-password: "Pokaż hasło" view-on-remote: "Dla dopełnienia, zobacz to zdalnie." @@ -218,8 +217,6 @@ auth/views/index.vue: please-go-back: "Wróć do aplikacji." error: "Sesja nie istnieje." sign-in: "Proszę zalogować się." -common/views/pages/explore.vue: - verified-users: "Zweryfikowane konto" common/views/components/games/reversi/reversi.vue: matching: waiting-for: "Oczekiwanie na {}" @@ -877,7 +874,6 @@ admin/views/instance.vue: invite: "Zaproś" save: "Zapisz" saved: "Zapisano" - user-recommendation-config: "Polecani użytkownicy" email: "Adres e-mail" admin/views/charts.vue: notes: "Wpisy" @@ -907,7 +903,6 @@ admin/views/users.vue: state: all: "Wszyscy" moderator: "Moderatorzy" - verified: "Zweryfikowane konto" origin: title: "Źródło" local: "Lokalny" @@ -953,6 +948,7 @@ admin/views/federation.vue: blocked: "Zablokuj" chart-srcs: requests: "Żądania" + blocked-hosts: "Zablokuj" desktop/views/pages/welcome.vue: about: "O Misskey" timeline: "Oś czasu" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 587aa248c..576f367d9 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -89,7 +89,6 @@ common: update-available-title: "Atualização disponível" update-available: "Uma nova versão de Misskey está disponível ({newer}). A versão atual é {current}. Recarregue a página para atualizar." my-token-regenerated: "Seu token foi recriado, portanto você foi deslogado." - verified-user: "Conta verificada" reversi: drawn: "Empatado" my-turn: "Seu turno" @@ -129,8 +128,6 @@ auth/views/index.vue: please-go-back: "Por favor, volte ao aplicativo." error: "A sessão não existe." sign-in: "Por favor, entre." -common/views/pages/explore.vue: - verified-users: "Conta verificada" common/views/components/games/reversi/reversi.index.vue: invite: "Convidar" rule: "Como jogar" @@ -194,10 +191,6 @@ admin/views/instance.vue: invite: "Convidar" admin/views/drive.vue: delete: "Apagar" -admin/views/users.vue: - users: - state: - verified: "Conta verificada" admin/views/emoji.vue: emojis: remove: "Apagar" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 2629aa71d..d9ecd0d2a 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -251,7 +251,6 @@ common: update-available-title: "有可用更新" update-available: "新的 Misskey 版本现已发布({newer}。目前版本{current}). 刷新页面以应用更新。" my-token-regenerated: "您的 Token 已被重置, 您将自动登出。" - verified-user: "认证用户" hide-password: "隐藏密码" show-password: "显示密码" do-not-use-in-production: "这是一个开发者测试版. 请勿在生产环境中使用." @@ -319,7 +318,7 @@ auth/views/index.vue: error: "会话不存在。" sign-in: "请登录。" common/views/pages/explore.vue: - verified-users: "官方账户" + pinned-users: "已置顶用户" popular-users: "热门用户" recently-updated-users: "活跃用户" recently-registered-users: "新用户" @@ -1139,7 +1138,7 @@ admin/views/instance.vue: invite: "邀请" save: "保存" saved: "保存完毕" - user-recommendation-config: "推荐用户" + pinned-users: "置顶用户" email-config: "电子邮件服务器设置" email-config-info: "用于确认电子邮件和密码重置等。" enable-email: "启用电子邮件送递" @@ -1223,12 +1222,6 @@ admin/views/users.vue: silence-confirm: "确认屏蔽?" unmake-silence: "解除禁言" unsilence-confirm: "解除屏蔽?" - verify: "认证用户" - verify-confirm: "是否官方账号?" - verified: "此账户已被认证" - unverify: "解除账户认证" - unverify-confirm: "是否解除官方账号认证?" - unverified: "该帐户未经认证" update-remote-user: "更新远程用户信息" remote-user-updated: "远程用户信息已更新" users: @@ -1245,7 +1238,6 @@ admin/views/users.vue: admin: "管理员" moderator: "版主" adminOrModerator: "管理员+版主" - verified: "官方认证账户" silenced: "已禁言" suspended: "已冻结" origin: @@ -1353,6 +1345,7 @@ admin/views/federation.vue: chart-spans: hour: "每小时" day: "每天" + blocked-hosts: "拉黑" desktop/views/pages/welcome.vue: about: "更多信息..." timeline: "时间线" @@ -1671,6 +1664,8 @@ pages: font: "字体" fontSerif: "衬线字体" fontSansSerif: "无衬线字体" + set-eye-catching-image: "设置封面图片" + remove-eye-catching-image: "删除封面图片" choose-block: "添加块" select-type: "类型选择" enter-variable-name: "请确定变量名" @@ -1735,6 +1730,7 @@ pages: value: "值" fn: "函数" text: "文本操作" + convert: "转换" list: "列表" blocks: text: "文本" @@ -1818,6 +1814,9 @@ pages: arg1: "如果" arg2: "的话" arg3: "否则" + not: "否定" + _not: + arg1: "否定" random: "随机" _random: arg1: "概率" @@ -1851,6 +1850,7 @@ pages: _seedRandomPick: arg1: "种子" arg2: "列表" + DRPWPM: "从概率列表中随机选择(每用户每天)" _DRPWPM: arg1: "文本列表" pick: "从列表中选择" From 13feaea7b7306cef2082242a0539f1e5aec4ecc9 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 14 May 2019 21:25:00 +0900 Subject: [PATCH 013/153] 11.13.0 --- CHANGELOG.md | 104 ++++++++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 63 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 391329c56..a17528251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,12 +73,32 @@ mongodb: 8. master ブランチに戻す 9. enjoy +11.13.0 (2019/05/14) +-------------------- +### 注意 +このアップデートを適用した後、プロセスを起動(もしくは再起動)する前に[マイグレーション](#migration)の手順を実行してください + +### ✨Improvements +* 利用規約URL、リポジトリURL、フィードバックURLを設定できるように +* 特定のインスタンスのファイルをすべて削除できるように +* _blankで外部リンクされる可能性がある箇所にnoopenerを追加 +* ユーザーや外部インスタンスが生成するリンクにnofollowを追加 +* リモートのユーザーページやノートページにnoindexを追加 +* 自分のユーザーメニューにはミュートなどを表示しないように + +### 🐛Fixes +* インスタンスブロックを設定できない問題を修正 +* ピン留め投稿の表示順がおかしい問題を修正 +* 設定の「アップデートを確認」でメッセージが正しく表示されない問題を修正 +* FFirefoxで自分のメニューが開けない問題を修正 +* Welcomeページのタグクラウドが動かない問題を修正 + 11.12.0 (2019/05/10) -------------------- ### 注意 このアップデートを適用した後、プロセスを起動(もしくは再起動)する前に[マイグレーション](#migration)の手順を実行してください -### Improvements +### ✨ Improvements * インスタンス運営者がおすすめアカウントを設定できるように * MisskeyPagesでNAME環境変数がNULLにならないように * MisskeyPagesにNULL環境変数を追加 @@ -86,7 +106,7 @@ mongodb: * MisskeyPagesのテキストのリスト内で変数埋め込みできるように * 自分の指定した投稿のRenoteを全て解除するAPIを追加 -### Fixes +### 🐛Fixes * Noteをpull取得した時にhost名がvalidateされていない問題を修正 * みつけるで人気のタグが表示されない問題を修正 @@ -95,7 +115,7 @@ mongodb: 11.11.2 (2019/05/07) -------------------- -### Fixes +### 🐛Fixes * IPv4 onlyホストからDualstackホストにAP deliverできない問題を修正 * ストリーミングに接続するまでラグがある問題を修正 * 2段階認証のコードが0から始まる時正しく入力できない問題を修正 @@ -106,24 +126,24 @@ mongodb: 11.11.1 (2019/05/05) -------------------- -### Fixes +### 🐛Fixes * MisskeyPagesのリストから選択関数が使えない問題を修正 11.11.0 (2019/05/05) -------------------- -### Improvements +### ✨ Improvements * MisskeyPagesにリストから選択関数を追加 * MisskeyPagesに確率を指定できるテキストランダム選択関数を追加 * 外部サービス連携ログインリンクにアイコン追加 -### Fixes +### 🐛Fixes * MisskeyPagesでifを入れ子にできなくなっていた問題を修正 * MisskeyPagesで数値入力を作成するとテキスト入力になる問題を修正 * 外部サービス連携に関する問題を修正 11.10.1 (2019/05/04) -------------------- -### Fixes +### 🐛Fixes * MisskeyPagesでページブロックを削除できなくなっていた問題を修正 ### その他 @@ -134,13 +154,13 @@ mongodb: ### 注意 このアップデートを適用した後、プロセスを起動(もしくは再起動)する前に[マイグレーション](#migration)の手順を実行してください -### Improvements +### ✨ Improvements * MisskeyPagesに割った余りを求める関数を追加 * Mastodon v2.8.0 のフォローリストをインポートできるように * エクスポートリクエストに失敗したらエラーを表示するように * エクスポートファイルでは同一ハッシュチェックをしないように -### Fixes +### 🐛Fixes * 2段階認証を設定するとログインできなくなる問題を修正 * ファイルをアップロードできないことがある問題を修正 * リモートファイルをキャッシュしない設定だとサムネイル時にオリジナル画像が表示されない問題を修正 @@ -148,13 +168,13 @@ mongodb: 11.9.0 (2019/05/02) ------------------- -### Improvements +### ✨ Improvements * MisskeyPagesで編集時にページブロックをドラッグで並べ替えられるように * MisskeyPagesにカウンターボタンブロックを追加 11.8.1 (2019/05/02) ------------------- -### Fixes +### 🐛Fixes * リモートファイルをキャッシュしないオプション有効時にファイルが作成できない問題を修正 11.8.0-2 (2019/05/01) @@ -163,20 +183,20 @@ mongodb: 11.8.0 (2019/05/01) ------------------- -### Improvements +### ✨ Improvements * MisskeyPagesで関数を作成できるように * MisskeyPagesでソースを表示できるように * MisskeyPagesにシードを与えるランダム関数を追加 * MisskeyPagesに複数行テキストをテキストのリストに変換する関数を追加 -### Fixes +### 🐛Fixes * APIドキュメントが見れなくなっていたのを修正 * mention (あなた宛て) streaming にミュートが効かない問題を修正 * デザインの調整 11.7.0 (2019/04/30) ------------------- -### Improvements +### ✨ Improvements * MisskeyPagesに ifブロック を追加 * MisskeyPagesに テキストエリア を追加 * MisskeyPagesに 複数行テキスト入力 を追加 @@ -185,23 +205,23 @@ mongodb: * MisskeyPagesに 環境変数 URL を追加 * MisskeyPagesでボタンやスイッチなどのテキストに変数使えるように -### Fixes +### 🐛Fixes * OGPのサイト名を修正 * デザインの調整 11.6.0 (2019/04/29) ------------------- -### Improvements +### ✨ Improvements * AiScriptにいくつかの文字列操作関数を追加 * ページ編集画面にページへのリンクを表示するように -### Fixes +### 🐛Fixes * MisskeyPagesで数値入力が文字列として扱われる問題を修正 * デザインの調整 11.5.1 (2019/04/29) ------------------- -### Fixes +### 🐛Fixes * MisskeyPagesで環境変数を別の変数内で使えない問題を修正 * MisskeyPagesで値が0の変数が表示されない問題を修正 @@ -227,21 +247,21 @@ mongodb: ページを気に入ったら「いいね」しよう (coming soon) -### Improvements +### ✨ Improvements * APIコンソールでパラメータテンプレートを表示するように -### Fixes +### 🐛Fixes * おすすめユーザーに自分自身が含まれる問題を修正 * ユーザーサジェストで表示名が変わらない問題を修正 11.4.0 (2019/04/25) ------------------- -### Improvements +### ✨ Improvements * 検索でローカルの投稿のみに絞れるように * 検索で特定のインスタンスの投稿のみに絞れるように * 検索で特定のユーザーの投稿のみに絞れるように -### Fixes +### 🐛Fixes * 投稿が増殖する問題を修正 * ストリームで過去の投稿が流れてくる問題を修正 * モバイル版のユーザーページで遷移してもユーザー名が変わらない問題を修正 @@ -249,43 +269,43 @@ mongodb: 11.3.1 (2019/04/24) ------------------- -### Fixes +### 🐛Fixes * Webからファイルがアップロードできない問題を修正 11.3.0 (2019/04/24) ------------------- -### Improvements +### ✨ Improvements * お知らせにMFMを使えるように * お知らせに画像を添付できるように -### Fixes +### 🐛Fixes * 投稿のタグ検索APIで大文字小文字が区別されていたのを修正 * 公開範囲がホームの投稿がグローバルTLに流れる問題を修正 * モバイルビューの投稿詳細にて acct が長いとアイコンが圧迫面接される問題を修正 11.2.2 (2019/04/22) ------------------- -### Fixes +### 🐛Fixes * 2段階認証を有効にするとログインできない問題を修正 * リモートユーザーの修復処理が自動的に実行されない問題を修正 * リモートユーザー情報が更新されない問題を修正 11.2.1 (2019/04/21) ------------------- -### Fixes +### 🐛Fixes * MEIDが25桁になっているのを修正 * リモートユーザー情報が更新されない問題を修正 11.2.0 (2019/04/18) ------------------- -### Improvements +### ✨ Improvements * 検索で日付(日時)を入力するとタイムラインをその時点まで遡るように * APIコンソールでエンドポイントをサジェストするように * モバイル版でドライブのメニューを使いやすく * サイレンス時に確認を表示するように * ユーザーメニューでブロックなどの操作を行う時に確認するように -### Fixes +### 🐛Fixes * アプリケーション連携画面でパーミッションが表示されない問題を修正 * アンケートウィジットでもMFMを使用するように * フォローしてないユーザーのホーム投稿がSTLに流れてくる問題を修正 @@ -294,7 +314,7 @@ mongodb: 11.1.6 (2019/04/18) ------------------- -### Fixes +### 🐛Fixes * 未認知ユーザーからActivityが飛んできた場合に処理できない問題を修正 * その投稿を見たのにも関わらずメンションインジケーターが点灯し続ける問題を修正 * ハッシュタグの判定を改善 @@ -302,14 +322,14 @@ mongodb: 11.1.5 (2019/04/17) ------------------- -### Fixes +### 🐛Fixes * ユーザー名に含まれているカスタム絵文字が表示されないことがある問題を修正 * 壁紙の設定ができない問題を修正 * デザインの調整 11.1.4 (2019/04/17) ------------------- -### Fixes +### 🐛Fixes * タイムライン取得時に削除されたファイルを添付している投稿が含まれているとサーバーでエラーになる問題を修正 * 管理画面のインスタンスメニューで変更前の設定が読み込まれないことがある問題を修正 * 猫ではないのに猫のままで表示される問題を修正 @@ -319,12 +339,12 @@ mongodb: 11.1.3 (2019/04/16) ------------------- -### Fixes +### 🐛Fixes * アプリからAPIにリクエストするときにランダムなユーザーがリクエストしたことになる問題を修正 11.1.2 (2019/04/15) ------------------- -### Fixes +### 🐛Fixes * 画像描画の依存関係を変更 * リモートユーザーのファイルを削除するときに古い方からではなく新しい方から削除されるのを修正 * リアクションしてないのにリアクションしたことになる問題を修正 @@ -332,25 +352,25 @@ mongodb: 11.1.1 (2019/04/15) ------------------- -### Fixes +### 🐛Fixes * Metaタグの application-name を Misskey で固定するように修正 * トークメッセージが既読にならない問題を修正 * デフォルトでHTLを表示するように 11.1.0 (2019/04/15) ------------------- -### Improvements +### ✨ Improvements * アイコン未設定時にランダムな画像を表示するように * 管理者やモデレーターはレートリミット無効に -### Fixes +### 🐛Fixes * メンションの「あなた」インジケーターが表示されない問題を修正 * ブロックAPIでエラーが発生する問題を修正 * プッシュ通知の購読に失敗する問題を修正 11.0.3 (2019/04/15) ------------------- -### Fixes +### 🐛Fixes * ハッシュタグ検索APIが動作しない問題を修正 * モデレーターなのにアカウントメニューに「管理」が表示されない問題を修正 * プッシュ通知の購読に失敗する問題を修正 @@ -358,7 +378,7 @@ mongodb: 11.0.2 (2019/04/15) ------------------- -### Fixes +### 🐛Fixes * アプリが作成できない問題を修正 * 「ハイライト」が表示されない問題を修正 * リモートの投稿に添付されている画像が小さい問題を修正 @@ -367,19 +387,19 @@ mongodb: 11.0.1 (2019/04/15) ------------------- -### Improvements +### ✨ Improvements * 不要な依存関係を削除 11.0.0 daybreak (2019/04/14) ---------------------------- -### Improvements +### ✨ Improvements * **データベースがMongoDBからPostgreSQLに変更されました** * **Redisが必須に** * アカウントを完全に削除できるように * 投稿フォームで添付ファイルの閲覧注意を確認/設定できるように * ミュート/ブロック時にそのユーザーの投稿のウォッチをすべて解除するように -### Fixes +### 🐛Fixes * フォロー申請数が実際より1すくなくなる問題を修正 * リストからアカウント削除したユーザーを削除できない問題を修正 * リストTLでフォローしていないユーザーの非公開投稿が流れる問題を修正 diff --git a/package.json b/package.json index 9a347fb19..71fa1509e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "misskey", "author": "syuilo ", - "version": "11.12.0", + "version": "11.13.0", "codename": "daybreak", "repository": { "type": "git", From 678d610cd674502aa41aac2d55f81870e6330c1c Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 14 May 2019 21:27:20 +0900 Subject: [PATCH 014/153] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a17528251..55e1f900b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,12 +85,13 @@ mongodb: * ユーザーや外部インスタンスが生成するリンクにnofollowを追加 * リモートのユーザーページやノートページにnoindexを追加 * 自分のユーザーメニューにはミュートなどを表示しないように +* デザインの調整 ### 🐛Fixes * インスタンスブロックを設定できない問題を修正 * ピン留め投稿の表示順がおかしい問題を修正 * 設定の「アップデートを確認」でメッセージが正しく表示されない問題を修正 -* FFirefoxで自分のメニューが開けない問題を修正 +* Firefoxで自分のメニューが開けない問題を修正 * Welcomeページのタグクラウドが動かない問題を修正 11.12.0 (2019/05/10) From e5409db0e82380d743907a538f60bd01434359eb Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 14 May 2019 23:54:39 +0900 Subject: [PATCH 015/153] Resolve #4925 --- CHANGELOG.md | 23 ++--------------------- CONTRIBUTING.md | 2 +- package.json | 2 ++ src/ormconfig.ts | 18 ++++++++++++++++++ 4 files changed, 23 insertions(+), 22 deletions(-) create mode 100644 src/ormconfig.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 55e1f900b..795bef150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,32 +8,13 @@ If you encounter any problems with updating, please try the following: Migration ------------------------------ #### 1 -`ormconfig.json`という名前で、Misskeyのインストール場所(package.jsonとかがあるディレクトリ)に新たなファイルを作る。中身は次のようにします: -``` json -{ - "type": "postgres", - "host": "PostgreSQLのホスト", - "port": 5432, - "username": "PostgreSQLのユーザー名", - "password": "PostgreSQLのパスワード", - "database": "PostgreSQLのデータベース名", - "entities": ["src/models/entities/*.ts"], - "migrations": ["migration/*.ts"], - "cli": { - "migrationsDir": "migration" - } -} -``` -上記の各種PostgreSQLの設定(ポートも)は、設定ファイルに書いてあるものをコピーしてください。 - -#### 2 ``` npm i -g ts-node ``` -#### 3 +#### 2 ``` -ts-node ./node_modules/typeorm/cli.js migration:run +npm run migrate ``` How to migrate to v11 from v10 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c0c72c470..ace822c63 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -199,7 +199,7 @@ const user = await Users.findOne(userId).then(ensure); ``` ### Migration作成方法 -コードの変更をした後、`ormconfig.json`(書き方はCONTRIBUTING.mdを参照)を用意し、 +コードの変更をした後、`ormconfig.json`(`npm run ormconfig`で生成)を用意し、 ``` npm i -g ts-node diff --git a/package.json b/package.json index 71fa1509e..5301118fd 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "scripts": { "start": "node ./index.js", "init": "node ./built/init.js", + "ormconfig": "node ./built/ormconfig.js", + "migrate": "npm run ormconfig && ts-node ./node_modules/typeorm/cli.js migration:run", "build": "webpack && gulp build", "webpack": "webpack", "watch": "webpack --watch", diff --git a/src/ormconfig.ts b/src/ormconfig.ts new file mode 100644 index 000000000..91f33181f --- /dev/null +++ b/src/ormconfig.ts @@ -0,0 +1,18 @@ +import * as fs from 'fs'; +import config from './config'; + +const json = { + type: 'postgres', + host: config.db.host, + port: config.db.port, + username: config.db.user, + password: config.db.pass, + database: config.db.db, + entities: ['src/models/entities/*.ts'], + migrations: ['migration/*.ts'], + cli: { + migrationsDir: 'migration' + } +}; + +fs.writeFileSync('ormconfig.json', JSON.stringify(json)); From a6befdd541a08f79686c505cc6dd6c92f32b8dbd Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 15 May 2019 17:05:41 +0900 Subject: [PATCH 016/153] Fix bug --- src/server/api/endpoints/admin/logs.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/server/api/endpoints/admin/logs.ts b/src/server/api/endpoints/admin/logs.ts index 86e99730c..060df09ad 100644 --- a/src/server/api/endpoints/admin/logs.ts +++ b/src/server/api/endpoints/admin/logs.ts @@ -53,16 +53,18 @@ export default define(meta, async (ps) => { if (blackDomains.length > 0) { query.andWhere(new Brackets(qb => { for (const blackDomain of blackDomains) { - const subDomains = blackDomain.split('.'); - let i = 0; - for (const subDomain of subDomains) { - const p = `blackSubDomain_${subDomain}_${i}`; - // 全体で否定できないのでド・モルガンの法則で - // !(P && Q) を !P || !Q で表す - // SQL is 1 based, so we need '+ 1' - qb.orWhere(`log.domain[${i + 1}] != :${p}`, { [p]: subDomain }); - i++; - } + qb.andWhere(new Brackets(qb => { + const subDomains = blackDomain.split('.'); + let i = 0; + for (const subDomain of subDomains) { + const p = `blackSubDomain_${subDomain}_${i}`; + // 全体で否定できないのでド・モルガンの法則で + // !(P && Q) を !P || !Q で表す + // SQL is 1 based, so we need '+ 1' + qb.orWhere(`log.domain[${i + 1}] != :${p}`, { [p]: subDomain }); + i++; + } + })); } })); } From 5ba8d4949d9a6956714932d5c0ea8f4d41ef7150 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 15 May 2019 20:29:47 +0900 Subject: [PATCH 017/153] =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=82=B9=E3=82=BF?= =?UTF-8?q?=E3=83=B3=E3=82=B9=E3=81=AE=E8=A8=AD=E5=AE=9A=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E3=82=92=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ja-JP.yml | 6 +- src/client/app/admin/views/hashtags.vue | 41 ------ src/client/app/admin/views/index.vue | 4 - src/client/app/admin/views/instance.vue | 163 ++++++++++++++++-------- 4 files changed, 116 insertions(+), 98 deletions(-) delete mode 100644 src/client/app/admin/views/hashtags.vue diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index ed0da44d6..91edb82e0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1187,7 +1187,6 @@ admin/views/index.vue: users: "ユーザー" federation: "連合" announcements: "お知らせ" - hashtags: "ハッシュタグ" abuse: "スパム報告" queue: "ジョブキュー" logs: "ログ" @@ -1230,6 +1229,8 @@ admin/views/instance.vue: maintainer-config: "管理者情報" maintainer-name: "管理者名" maintainer-email: "管理者の連絡先" + advanced-config: "その他の設定" + note-and-tl: "投稿とタイムライン" drive-config: "ドライブの設定" cache-remote-files: "リモートのファイルをキャッシュする" cache-remote-files-desc: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。そのためサーバーのストレージを節約できますが、プライバシー設定で直リンクを無効にしているユーザーにはファイルが見えなくなったり、サムネイルが生成されないので通信量が増加します。通常はこの設定をオンにしておくことをおすすめします。" @@ -1241,6 +1242,9 @@ admin/views/instance.vue: enable-recaptcha: "reCAPTCHAを有効にする" recaptcha-site-key: "reCAPTCHA site key" recaptcha-secret-key: "reCAPTCHA secret key" + hidden-tags: "非表示ハッシュタグ" + hidden-tags-info: "集計から除外するハッシュタグを改行で区切って記述します。" + external-service-integration-config: "外部サービス連携" twitter-integration-config: "Twitter連携の設定" twitter-integration-info: "コールバックURLは {url} に設定します。" enable-twitter-integration: "Twitter連携を有効にする" diff --git a/src/client/app/admin/views/hashtags.vue b/src/client/app/admin/views/hashtags.vue deleted file mode 100644 index e1cc4b494..000000000 --- a/src/client/app/admin/views/hashtags.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - diff --git a/src/client/app/admin/views/index.vue b/src/client/app/admin/views/index.vue index 4bce197ed..43e47038f 100644 --- a/src/client/app/admin/views/index.vue +++ b/src/client/app/admin/views/index.vue @@ -28,7 +28,6 @@
  • {{ $t('federation') }}
  • {{ $t('emoji') }}
  • {{ $t('announcements') }}
  • -
  • {{ $t('hashtags') }}
  • {{ $t('abuse') }}
  • @@ -48,7 +47,6 @@
    -
    @@ -68,7 +66,6 @@ import XLogs from "./logs.vue"; import XModerators from "./moderators.vue"; import XEmoji from "./emoji.vue"; import XAnnouncements from "./announcements.vue"; -import XHashtags from "./hashtags.vue"; import XUsers from "./users.vue"; import XDrive from "./drive.vue"; import XAbuse from "./abuse.vue"; @@ -91,7 +88,6 @@ export default Vue.extend({ XModerators, XEmoji, XAnnouncements, - XHashtags, XUsers, XDrive, XAbuse, diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue index 5cdd22296..be9e56131 100644 --- a/src/client/app/admin/views/instance.vue +++ b/src/client/app/admin/views/instance.vue @@ -2,7 +2,7 @@
    -
    +
    {{ $t('host') }} {{ $t('instance-name') }} {{ $t('instance-description') }} @@ -11,49 +11,63 @@ {{ $t('banner-url') }} {{ $t('error-image-url') }} {{ $t('tos-url') }} - {{ $t('repository-url') }} - {{ $t('feedback-url') }} {{ $t('languages') }} +
    + {{ $t('advanced-config') }} + {{ $t('repository-url') }} + {{ $t('feedback-url') }} +
    {{ $t('maintainer-config') }}
    {{ $t('maintainer-name') }} {{ $t('maintainer-email') }}
    +
    + {{ $t('disable-registration') }} + {{ $t('invite') }} +
    +
    + {{ $t('save') }} +
    + + + +
    {{ $t('max-note-text-length') }}
    - {{ $t('disable-registration') }} {{ $t('disable-local-timeline') }} {{ $t('disable-global-timeline') }} {{ $t('disabling-timelines-info') }} +
    +
    {{ $t('enable-emoji-reaction') }} {{ $t('use-star-for-reaction-fallback') }}
    -
    -
    {{ $t('drive-config') }}
    +
    + {{ $t('save') }} +
    + + + + +
    {{ $t('cache-remote-files') }} +
    +
    {{ $t('local-drive-capacity-mb') }} {{ $t('remote-drive-capacity-mb') }}
    -
    -
    {{ $t('recaptcha-config') }}
    - {{ $t('enable-recaptcha') }} - {{ $t('recaptcha-info') }} - - {{ $t('recaptcha-site-key') }} - {{ $t('recaptcha-secret-key') }} - -
    -
    {{ $t('proxy-account-config') }}
    - {{ $t('proxy-account-info') }} - {{ $t('proxy-account-username') }} - {{ $t('proxy-account-warn') }} + {{ $t('save') }}
    +
    + + +
    -
    {{ $t('email-config') }}
    {{ $t('enable-email') }} {{ $t('email') }} @@ -63,12 +77,30 @@ {{ $t('smtp-auth') }} {{ $t('smtp-user') }} - {{ $t('smtp-pass') }} + {{ $t('smtp-pass') }} {{ $t('smtp-secure') }}
    -
    {{ $t('serviceworker-config') }}
    + {{ $t('save') }} +
    +
    + + + +
    + {{ $t('proxy-account-info') }} + {{ $t('proxy-account-username') }} + {{ $t('proxy-account-warn') }} +
    +
    + {{ $t('save') }} +
    +
    + + + +
    {{ $t('enable-serviceworker') }} {{ $t('vapid-info') }}
    npm i web-push -g
    web-push generate-vapid-keys
    @@ -77,11 +109,22 @@
    -
    summaly Proxy
    - URL + {{ $t('save') }} +
    +
    + + + +
    + {{ $t('enable-recaptcha') }} + {{ $t('recaptcha-info') }} + + {{ $t('recaptcha-site-key') }} + {{ $t('recaptcha-secret-key') }} +
    - {{ $t('save') }} + {{ $t('save') }}
    @@ -91,56 +134,67 @@ - {{ $t('save') }} + {{ $t('save') }}
    - -
    - {{ $t('invite') }} -

    Code: {{ inviteCode }}

    -
    -
    - - - +
    +
    {{ $t('twitter-integration-config') }}
    {{ $t('enable-twitter-integration') }} {{ $t('twitter-integration-consumer-key') }} {{ $t('twitter-integration-consumer-secret') }} {{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }} - {{ $t('save') }}
    -
    - - -
    +
    {{ $t('github-integration-config') }}
    {{ $t('enable-github-integration') }} {{ $t('github-integration-client-id') }} {{ $t('github-integration-client-secret') }} {{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }} - {{ $t('save') }}
    -
    - - -
    +
    {{ $t('discord-integration-config') }}
    {{ $t('enable-discord-integration') }} {{ $t('discord-integration-client-id') }} {{ $t('discord-integration-client-secret') }} {{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }} - {{ $t('save') }} +
    +
    + {{ $t('save') }}
    + +
    + {{ $t('advanced-config') }} + + + +
    + + + + {{ $t('save') }} +
    +
    + + + +
    + URL +
    +
    + {{ $t('save') }} +
    +
    +
    @@ -149,8 +203,8 @@ import Vue from 'vue'; import i18n from '../../i18n'; import { url, host } from '../../config'; import { toUnicode } from 'punycode'; -import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt, faThumbtack } from '@fortawesome/free-solid-svg-icons'; -import { faEnvelope as farEnvelope } from '@fortawesome/free-regular-svg-icons'; +import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt, faThumbtack, faPencilAlt, faHashtag } from '@fortawesome/free-solid-svg-icons'; +import { faEnvelope as farEnvelope, faSave } from '@fortawesome/free-regular-svg-icons'; export default Vue.extend({ i18n: i18n('admin/views/instance.vue'), @@ -193,7 +247,6 @@ export default Vue.extend({ discordClientId: null, discordClientSecret: null, proxyAccount: null, - inviteCode: null, summalyProxy: null, enableEmail: false, email: null, @@ -207,7 +260,8 @@ export default Vue.extend({ swPublicKey: null, swPrivateKey: null, pinnedUsers: '', - faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt, faThumbtack + hiddenTags: '', + faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt, faThumbtack, faPencilAlt, faSave, faHashtag }; }, @@ -260,13 +314,17 @@ export default Vue.extend({ this.swPublicKey = meta.swPublickey; this.swPrivateKey = meta.swPrivateKey; this.pinnedUsers = meta.pinnedUsers.join('\n'); + this.hiddenTags = meta.hiddenTags.join('\n'); }); }, methods: { invite() { this.$root.api('admin/invite').then(x => { - this.inviteCode = x.code; + this.$root.dialog({ + type: 'info', + text: x.code + }); }).catch(e => { this.$root.dialog({ type: 'error', @@ -322,7 +380,8 @@ export default Vue.extend({ enableServiceWorker: this.enableServiceWorker, swPublicKey: this.swPublicKey, swPrivateKey: this.swPrivateKey, - pinnedUsers: this.pinnedUsers.split('\n') + pinnedUsers: this.pinnedUsers.split('\n'), + hiddenTags: this.hiddenTags.split('\n'), }).then(() => { this.$root.dialog({ type: 'success', From 23c9f6a6ca1ba938a6a2a7c9f5e1007d47fd28c5 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 15 May 2019 20:41:01 +0900 Subject: [PATCH 018/153] Resolve #4833 --- locales/ja-JP.yml | 2 ++ src/client/app/admin/views/users.vue | 28 +++++++++++++--- .../admin/delete-all-files-of-a-user.ts | 32 +++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 src/server/api/endpoints/admin/delete-all-files-of-a-user.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 91edb82e0..76c1ab826 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1365,6 +1365,8 @@ admin/views/users.vue: unsilence-confirm: "サイレンスを解除しますか?" update-remote-user: "リモートユーザー情報の更新" remote-user-updated: "リモートユーザー情報を更新しました" + delete-all-files: "すべてのファイルを削除" + delete-all-files-confirm: "すべてのファイルを削除しますか?" users: title: "ユーザー" sort: diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue index cc3810853..fd9f0dd8b 100644 --- a/src/client/app/admin/views/users.vue +++ b/src/client/app/admin/views/users.vue @@ -9,8 +9,9 @@ {{ $t('lookup') }}
    - +
    + {{ $t('update-remote-user') }} {{ $t('reset-password') }} {{ $t('make-silence') }} @@ -20,7 +21,7 @@ {{ $t('suspend') }} {{ $t('unsuspend') }} - {{ $t('update-remote-user') }} + {{ $t('delete-all-files') }}
    @@ -67,7 +68,7 @@ import Vue from 'vue'; import i18n from '../../i18n'; import parseAcct from "../../../../misc/acct/parse"; import { faUsers, faTerminal, faSearch, faKey, faSync, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'; -import { faSnowflake } from '@fortawesome/free-regular-svg-icons'; +import { faSnowflake, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; import XUser from './users.user.vue'; export default Vue.extend({ @@ -88,7 +89,7 @@ export default Vue.extend({ offset: 0, users: [], existMore: false, - faTerminal, faUsers, faSnowflake, faSearch, faKey, faSync, faMicrophoneSlash + faTerminal, faUsers, faSnowflake, faSearch, faKey, faSync, faMicrophoneSlash, faTrashAlt }; }, @@ -277,6 +278,25 @@ export default Vue.extend({ this.refreshUser(); }, + async deleteAllFiles() { + if (!await this.getConfirmed(this.$t('delete-all-files-confirm'))) return; + + const process = async () => { + await this.$root.api('admin/delete-all-files-of-a-user', { userId: this.user.id }); + this.$root.dialog({ + type: 'success', + splash: true + }); + }; + + await process().catch(e => { + this.$root.dialog({ + type: 'error', + text: e.toString() + }); + }); + }, + async getConfirmed(text: string): Promise { const confirm = await this.$root.dialog({ type: 'warning', diff --git a/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts new file mode 100644 index 000000000..84e9c363e --- /dev/null +++ b/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts @@ -0,0 +1,32 @@ +import $ from 'cafy'; +import define from '../../define'; +import del from '../../../../services/drive/delete-file'; +import { DriveFiles } from '../../../../models'; +import { ID } from '../../../../misc/cafy-id'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + userId: { + validator: $.type(ID), + desc: { + 'ja-JP': '対象のユーザーID', + 'en-US': 'The user ID which you want to suspend' + } + }, + } +}; + +export default define(meta, async (ps, me) => { + const files = await DriveFiles.find({ + userId: ps.userId + }); + + for (const file of files) { + del(file); + } +}); From 3d8bbedf1bbd501d3ad5e3c26e5e5005ed2d8371 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 15 May 2019 21:27:20 +0900 Subject: [PATCH 019/153] =?UTF-8?q?GIF=E3=81=AE=E3=82=B5=E3=83=A0=E3=83=8D?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E3=81=8C=E7=94=9F=E6=88=90=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=81=AA=E3=81=84=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #4728 --- src/server/proxy/proxy-media.ts | 6 ++-- src/services/drive/add-file.ts | 18 ++++++---- .../drive/generate-video-thumbnail.ts | 4 +-- src/services/drive/image-processor.ts | 33 +++++++++++++++++-- 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/server/proxy/proxy-media.ts b/src/server/proxy/proxy-media.ts index 357715bb9..e16665f6c 100644 --- a/src/server/proxy/proxy-media.ts +++ b/src/server/proxy/proxy-media.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as Koa from 'koa'; import { serverLogger } from '..'; -import { IImage, ConvertToPng, ConvertToJpeg } from '../../services/drive/image-processor'; +import { IImage, convertToPng, convertToJpeg } from '../../services/drive/image-processor'; import { createTemp } from '../../misc/create-temp'; import { downloadUrl } from '../../misc/donwload-url'; import { detectMine } from '../../misc/detect-mine'; @@ -20,9 +20,9 @@ export async function proxyMedia(ctx: Koa.BaseContext) { let image: IImage; if ('static' in ctx.query && ['image/png', 'image/gif'].includes(type)) { - image = await ConvertToPng(path, 498, 280); + image = await convertToPng(path, 498, 280); } else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif'].includes(type)) { - image = await ConvertToJpeg(path, 200, 200); + image = await convertToJpeg(path, 200, 200); } else { image = { data: fs.readFileSync(path), diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index c67ee475a..949089ede 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -12,7 +12,7 @@ import config from '../../config'; import { fetchMeta } from '../../misc/fetch-meta'; import { GenerateVideoThumbnail } from './generate-video-thumbnail'; import { driveLogger } from './logger'; -import { IImage, ConvertToJpeg, ConvertToWebp, ConvertToPng } from './image-processor'; +import { IImage, convertToJpeg, convertToWebp, convertToPng, convertToGif, convertToApng } from './image-processor'; import { contentDisposition } from '../../misc/content-disposition'; import { detectMine } from '../../misc/detect-mine'; import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '../../models'; @@ -149,11 +149,15 @@ export async function generateAlts(path: string, type: string, generateWeb: bool logger.info(`creating web image`); if (['image/jpeg'].includes(type)) { - webpublic = await ConvertToJpeg(path, 2048, 2048); + webpublic = await convertToJpeg(path, 2048, 2048); } else if (['image/webp'].includes(type)) { - webpublic = await ConvertToWebp(path, 2048, 2048); + webpublic = await convertToWebp(path, 2048, 2048); } else if (['image/png'].includes(type)) { - webpublic = await ConvertToPng(path, 2048, 2048); + webpublic = await convertToPng(path, 2048, 2048); + } else if (['image/apng', 'image/vnd.mozilla.apng'].includes(type)) { + webpublic = await convertToApng(path); + } else if (['image/gif'].includes(type)) { + webpublic = await convertToGif(path); } else { logger.info(`web image not created (not an image)`); } @@ -166,9 +170,11 @@ export async function generateAlts(path: string, type: string, generateWeb: bool let thumbnail: IImage | null = null; if (['image/jpeg', 'image/webp'].includes(type)) { - thumbnail = await ConvertToJpeg(path, 498, 280); + thumbnail = await convertToJpeg(path, 498, 280); } else if (['image/png'].includes(type)) { - thumbnail = await ConvertToPng(path, 498, 280); + thumbnail = await convertToPng(path, 498, 280); + } else if (['image/gif'].includes(type)) { + thumbnail = await convertToGif(path); } else if (type.startsWith('video/')) { try { thumbnail = await GenerateVideoThumbnail(path); diff --git a/src/services/drive/generate-video-thumbnail.ts b/src/services/drive/generate-video-thumbnail.ts index 5d7efff27..c2646182d 100644 --- a/src/services/drive/generate-video-thumbnail.ts +++ b/src/services/drive/generate-video-thumbnail.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as tmp from 'tmp'; -import { IImage, ConvertToJpeg } from './image-processor'; +import { IImage, convertToJpeg } from './image-processor'; const ThumbnailGenerator = require('video-thumbnail-generator').default; export async function GenerateVideoThumbnail(path: string): Promise { @@ -23,7 +23,7 @@ export async function GenerateVideoThumbnail(path: string): Promise { const outPath = `${outDir}/output.png`; - const thumbnail = await ConvertToJpeg(outPath, 498, 280); + const thumbnail = await convertToJpeg(outPath, 498, 280); // cleanup fs.unlinkSync(outPath); diff --git a/src/services/drive/image-processor.ts b/src/services/drive/image-processor.ts index 89ac331ca..4b8db0e0c 100644 --- a/src/services/drive/image-processor.ts +++ b/src/services/drive/image-processor.ts @@ -1,4 +1,5 @@ import * as sharp from 'sharp'; +import * as fs from 'fs'; export type IImage = { data: Buffer; @@ -10,7 +11,7 @@ export type IImage = { * Convert to JPEG * with resize, remove metadata, resolve orientation, stop animation */ -export async function ConvertToJpeg(path: string, width: number, height: number): Promise { +export async function convertToJpeg(path: string, width: number, height: number): Promise { const data = await sharp(path) .resize(width, height, { fit: 'inside', @@ -34,7 +35,7 @@ export async function ConvertToJpeg(path: string, width: number, height: number) * Convert to WebP * with resize, remove metadata, resolve orientation, stop animation */ -export async function ConvertToWebp(path: string, width: number, height: number): Promise { +export async function convertToWebp(path: string, width: number, height: number): Promise { const data = await sharp(path) .resize(width, height, { fit: 'inside', @@ -57,7 +58,7 @@ export async function ConvertToWebp(path: string, width: number, height: number) * Convert to PNG * with resize, remove metadata, resolve orientation, stop animation */ -export async function ConvertToPng(path: string, width: number, height: number): Promise { +export async function convertToPng(path: string, width: number, height: number): Promise { const data = await sharp(path) .resize(width, height, { fit: 'inside', @@ -73,3 +74,29 @@ export async function ConvertToPng(path: string, width: number, height: number): type: 'image/png' }; } + +/** + * Convert to GIF (Actually just NOP) + */ +export async function convertToGif(path: string): Promise { + const data = await fs.promises.readFile(path); + + return { + data, + ext: 'gif', + type: 'image/gif' + }; +} + +/** + * Convert to APNG (Actually just NOP) + */ +export async function convertToApng(path: string): Promise { + const data = await fs.promises.readFile(path); + + return { + data, + ext: 'apng', + type: 'image/apng' + }; +} From 3f5b96bf629da5f736c09b10058802eed28cca18 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 16 May 2019 01:07:32 +0900 Subject: [PATCH 020/153] Resolve #4928 --- .circleci/misskey/default.yml | 2 - .circleci/misskey/test.yml | 2 - .config/example.yml | 55 ----- locales/ja-JP.yml | 13 + .../1557932705754-ObjectStorageSetting.ts | 31 +++ src/client/app/admin/views/instance.vue | 224 ++++++++++++------ src/config/types.ts | 7 - src/models/entities/meta.ts | 57 +++++ src/server/api/endpoints/admin/update-meta.ts | 82 ++++++- src/server/api/endpoints/meta.ts | 12 +- src/services/drive/add-file.ts | 27 ++- src/services/drive/delete-file.ts | 18 +- 12 files changed, 370 insertions(+), 160 deletions(-) create mode 100644 migration/1557932705754-ObjectStorageSetting.ts diff --git a/.circleci/misskey/default.yml b/.circleci/misskey/default.yml index c842431d2..5cdb7330c 100644 --- a/.circleci/misskey/default.yml +++ b/.circleci/misskey/default.yml @@ -6,8 +6,6 @@ mongodb: db: misskey user: syuilo pass: '' -drive: - storage: 'db' redis: host: localhost port: 6379 diff --git a/.circleci/misskey/test.yml b/.circleci/misskey/test.yml index 450c5a79d..99ad50876 100644 --- a/.circleci/misskey/test.yml +++ b/.circleci/misskey/test.yml @@ -6,8 +6,6 @@ mongodb: db: test-misskey user: admin pass: '' -drive: - storage: 'db' # __REDIS__ redis: host: localhost diff --git a/.config/example.yml b/.config/example.yml index db278ecc2..0babd037c 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -78,61 +78,6 @@ redis: # port: 9200 # pass: null -# ┌────────────────────────────────────┐ -#───┘ File storage (Drive) configuration └────────────────────── - -drive: - storage: 'fs' - -# OR - -#drive: -# storage: 'minio' -# bucket: -# prefix: -# config: -# endPoint: -# port: -# useSSL: -# accessKey: -# secretKey: - -# S3/GCS example -# -# * Replace to -# S3: see https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region -# GCS: use 'storage.googleapis.com' -# -# * Replace to -# S3: see https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region -# GCS: not needed (just delete the region line) -# -#drive: -# storage: 'minio' -# bucket: bucket-name -# prefix: files -# baseUrl: https://bucket-name. -# config: -# endPoint: -# region: -# useSSL: true -# accessKey: XXX -# secretKey: YYY - -# S3/GCS example (with CDN, custom domain) -# -#drive: -# storage: 'minio' -# bucket: drive.example.com -# prefix: files -# baseUrl: https://drive.example.com -# config: -# endPoint: -# region: -# useSSL: true -# accessKey: XXX -# secretKey: YYY - # ┌───────────────┐ #───┘ ID generation └─────────────────────────────────────────── diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 76c1ab826..bb991459c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1232,6 +1232,19 @@ admin/views/instance.vue: advanced-config: "その他の設定" note-and-tl: "投稿とタイムライン" drive-config: "ドライブの設定" + use-object-storage: "オブジェクトストレージを使用する" + object-storage-base-url: "URL" + object-storage-bucket: "バケット名" + object-storage-prefix: "プレフィックス" + object-storage-endpoint: "エンドポイント" + object-storage-region: "リージョン" + object-storage-port: "ポート" + object-storage-access-key: "アクセスキー" + object-storage-secret-key: "シークレットキー" + object-storage-use-ssl: "SSLを使用" + object-storage-s3-info: "Amazon S3をオブジェクトストレージとして使用する場合の「エンドポイント」と「リージョン」の設定については{0}をご確認ください。" + object-storage-s3-info-here: "こちら" + object-storage-gcs-info: "Google Cloud Storageをオブジェクトストレージとして使用する場合、「エンドポイント」は storage.googleapis.com に設定し、「リージョン」は空欄にします。" cache-remote-files: "リモートのファイルをキャッシュする" cache-remote-files-desc: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。そのためサーバーのストレージを節約できますが、プライバシー設定で直リンクを無効にしているユーザーにはファイルが見えなくなったり、サムネイルが生成されないので通信量が増加します。通常はこの設定をオンにしておくことをおすすめします。" local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量" diff --git a/migration/1557932705754-ObjectStorageSetting.ts b/migration/1557932705754-ObjectStorageSetting.ts new file mode 100644 index 000000000..dde6aa65f --- /dev/null +++ b/migration/1557932705754-ObjectStorageSetting.ts @@ -0,0 +1,31 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class ObjectStorageSetting1557932705754 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "meta" ADD "useObjectStorage" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageBucket" character varying(512)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "objectStoragePrefix" character varying(512)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageBaseUrl" character varying(512)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageEndpoint" character varying(512)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageRegion" character varying(512)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageAccessKey" character varying(512)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageSecretKey" character varying(512)`); + await queryRunner.query(`ALTER TABLE "meta" ADD "objectStoragePort" integer`); + await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageUseSSL" boolean NOT NULL DEFAULT true`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageUseSSL"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStoragePort"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageSecretKey"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageAccessKey"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRegion"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageEndpoint"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageBaseUrl"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStoragePrefix"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageBucket"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useObjectStorage"`); + } + +} diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue index be9e56131..3ac4d6d72 100644 --- a/src/client/app/admin/views/instance.vue +++ b/src/client/app/admin/views/instance.vue @@ -53,6 +53,32 @@ +
    + {{ $t('use-object-storage') }} + +
    {{ $t('cache-remote-files') }}
    @@ -65,69 +91,6 @@
    - - -
    - {{ $t('enable-email') }} - {{ $t('email') }} - - {{ $t('smtp-host') }} - {{ $t('smtp-port') }} - - {{ $t('smtp-auth') }} - - {{ $t('smtp-user') }} - {{ $t('smtp-pass') }} - - {{ $t('smtp-secure') }} -
    -
    - {{ $t('save') }} -
    -
    - - - -
    - {{ $t('proxy-account-info') }} - {{ $t('proxy-account-username') }} - {{ $t('proxy-account-warn') }} -
    -
    - {{ $t('save') }} -
    -
    - - - -
    - {{ $t('enable-serviceworker') }} - {{ $t('vapid-info') }}
    npm i web-push -g
    web-push generate-vapid-keys
    - - {{ $t('vapid-publickey') }} - {{ $t('vapid-privatekey') }} - -
    -
    - {{ $t('save') }} -
    -
    - - - -
    - {{ $t('enable-recaptcha') }} - {{ $t('recaptcha-info') }} - - {{ $t('recaptcha-site-key') }} - {{ $t('recaptcha-secret-key') }} - -
    -
    - {{ $t('save') }} -
    -
    -
    @@ -138,34 +101,109 @@
    + + +
    + {{ $t('proxy-account-info') }} + {{ $t('proxy-account-username') }} + {{ $t('proxy-account-warn') }} +
    +
    + {{ $t('save') }} +
    +
    + + + +
    + {{ $t('enable-email') }} + +
    +
    + {{ $t('save') }} +
    +
    + + + +
    + {{ $t('enable-serviceworker') }} + +
    +
    + {{ $t('save') }} +
    +
    + + + +
    + {{ $t('enable-recaptcha') }} + +
    +
    + {{ $t('save') }} +
    +
    +
    {{ $t('twitter-integration-config') }}
    {{ $t('enable-twitter-integration') }} - - {{ $t('twitter-integration-consumer-key') }} - {{ $t('twitter-integration-consumer-secret') }} - - {{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }} +
    {{ $t('github-integration-config') }}
    {{ $t('enable-github-integration') }} - - {{ $t('github-integration-client-id') }} - {{ $t('github-integration-client-secret') }} - - {{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }} +
    {{ $t('discord-integration-config') }}
    {{ $t('enable-discord-integration') }} - - {{ $t('discord-integration-client-id') }} - {{ $t('discord-integration-client-secret') }} - - {{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }} +
    {{ $t('save') }} @@ -261,6 +299,16 @@ export default Vue.extend({ swPrivateKey: null, pinnedUsers: '', hiddenTags: '', + useObjectStorage: false, + objectStorageBaseUrl: null, + objectStorageBucket: null, + objectStoragePrefix: null, + objectStorageEndpoint: null, + objectStorageRegion: null, + objectStoragePort: null, + objectStorageAccessKey: null, + objectStorageSecretKey: null, + objectStorageUseSSL: false, faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt, faThumbtack, faPencilAlt, faSave, faHashtag }; }, @@ -315,6 +363,16 @@ export default Vue.extend({ this.swPrivateKey = meta.swPrivateKey; this.pinnedUsers = meta.pinnedUsers.join('\n'); this.hiddenTags = meta.hiddenTags.join('\n'); + this.useObjectStorage = meta.useObjectStorage; + this.objectStorageBaseUrl = meta.objectStorageBaseUrl; + this.objectStorageBucket = meta.objectStorageBucket; + this.objectStoragePrefix = meta.objectStoragePrefix; + this.objectStorageEndpoint = meta.objectStorageEndpoint; + this.objectStorageRegion = meta.objectStorageRegion; + this.objectStoragePort = meta.objectStoragePort; + this.objectStorageAccessKey = meta.objectStorageAccessKey; + this.objectStorageSecretKey = meta.objectStorageSecretKey; + this.objectStorageUseSSL = meta.objectStorageUseSSL; }); }, @@ -382,6 +440,16 @@ export default Vue.extend({ swPrivateKey: this.swPrivateKey, pinnedUsers: this.pinnedUsers.split('\n'), hiddenTags: this.hiddenTags.split('\n'), + useObjectStorage: this.useObjectStorage, + objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null, + objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null, + objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null, + objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null, + objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null, + objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null, + objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null, + objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null, + objectStorageUseSSL: this.objectStorageUseSSL, }).then(() => { this.$root.dialog({ type: 'success', diff --git a/src/config/types.ts b/src/config/types.ts index d312a5a18..7da9820f2 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -27,13 +27,6 @@ export type Source = { port: number; pass: string; }; - drive?: { - storage: string; - bucket?: string; - prefix?: string; - baseUrl?: string; - config?: any; - }; autoAdmin?: boolean; diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts index c3797a9ed..fdd281823 100644 --- a/src/models/entities/meta.ts +++ b/src/models/entities/meta.ts @@ -288,4 +288,61 @@ export class Meta { nullable: true }) public feedbackUrl: string | null; + + @Column('boolean', { + default: false, + }) + public useObjectStorage: boolean; + + @Column('varchar', { + length: 512, + nullable: true + }) + public objectStorageBucket: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public objectStoragePrefix: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public objectStorageBaseUrl: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public objectStorageEndpoint: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public objectStorageRegion: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public objectStorageAccessKey: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public objectStorageSecretKey: string | null; + + @Column('integer', { + nullable: true + }) + public objectStoragePort: number | null; + + @Column('boolean', { + default: true, + }) + public objectStorageUseSSL: boolean; } diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index e4f2e86aa..8e98d203f 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -357,7 +357,47 @@ export const meta = { desc: { 'ja-JP': 'フィードバックのURL' } - } + }, + + useObjectStorage: { + validator: $.optional.bool + }, + + objectStorageBaseUrl: { + validator: $.optional.nullable.str + }, + + objectStorageBucket: { + validator: $.optional.nullable.str + }, + + objectStoragePrefix: { + validator: $.optional.nullable.str + }, + + objectStorageEndpoint: { + validator: $.optional.nullable.str + }, + + objectStorageRegion: { + validator: $.optional.nullable.str + }, + + objectStoragePort: { + validator: $.optional.nullable.num + }, + + objectStorageAccessKey: { + validator: $.optional.nullable.str + }, + + objectStorageSecretKey: { + validator: $.optional.nullable.str + }, + + objectStorageUseSSL: { + validator: $.optional.bool + }, } }; @@ -560,6 +600,46 @@ export default define(meta, async (ps) => { set.feedbackUrl = ps.feedbackUrl; } + if (ps.useObjectStorage !== undefined) { + set.useObjectStorage = ps.useObjectStorage; + } + + if (ps.objectStorageBaseUrl !== undefined) { + set.objectStorageBaseUrl = ps.objectStorageBaseUrl; + } + + if (ps.objectStorageBucket !== undefined) { + set.objectStorageBucket = ps.objectStorageBucket; + } + + if (ps.objectStoragePrefix !== undefined) { + set.objectStoragePrefix = ps.objectStoragePrefix; + } + + if (ps.objectStorageEndpoint !== undefined) { + set.objectStorageEndpoint = ps.objectStorageEndpoint; + } + + if (ps.objectStorageRegion !== undefined) { + set.objectStorageRegion = ps.objectStorageRegion; + } + + if (ps.objectStoragePort !== undefined) { + set.objectStoragePort = ps.objectStoragePort; + } + + if (ps.objectStorageAccessKey !== undefined) { + set.objectStorageAccessKey = ps.objectStorageAccessKey; + } + + if (ps.objectStorageSecretKey !== undefined) { + set.objectStorageSecretKey = ps.objectStorageSecretKey; + } + + if (ps.objectStorageUseSSL !== undefined) { + set.objectStorageUseSSL = ps.objectStorageUseSSL; + } + await getConnection().transaction(async transactionalEntityManager => { const meta = await transactionalEntityManager.findOne(Meta, { order: { diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 1bd88a1e6..4f418c63c 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -153,7 +153,7 @@ export default define(meta, async (ps, me) => { globalTimeLine: !instance.disableGlobalTimeline, elasticsearch: config.elasticsearch ? true : false, recaptcha: instance.enableRecaptcha, - objectStorage: config.drive && config.drive.storage === 'minio', + objectStorage: instance.useObjectStorage, twitter: instance.enableTwitterIntegration, github: instance.enableGithubIntegration, discord: instance.enableDiscordIntegration, @@ -182,6 +182,16 @@ export default define(meta, async (ps, me) => { response.smtpUser = instance.smtpUser; response.smtpPass = instance.smtpPass; response.swPrivateKey = instance.swPrivateKey; + response.useObjectStorage = instance.useObjectStorage; + response.objectStorageBaseUrl = instance.objectStorageBaseUrl; + response.objectStorageBucket = instance.objectStorageBucket; + response.objectStoragePrefix = instance.objectStoragePrefix; + response.objectStorageEndpoint = instance.objectStorageEndpoint; + response.objectStorageRegion = instance.objectStorageRegion; + response.objectStoragePort = instance.objectStoragePort; + response.objectStorageAccessKey = instance.objectStorageAccessKey; + response.objectStorageSecretKey = instance.objectStorageSecretKey; + response.objectStorageUseSSL = instance.objectStorageUseSSL; } return response; diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index 949089ede..701878b28 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -8,7 +8,6 @@ import * as sharp from 'sharp'; import { publishMainStream, publishDriveStream } from '../stream'; import delFile from './delete-file'; -import config from '../../config'; import { fetchMeta } from '../../misc/fetch-meta'; import { GenerateVideoThumbnail } from './generate-video-thumbnail'; import { driveLogger } from './logger'; @@ -37,7 +36,9 @@ async function save(file: DriveFile, path: string, name: string, type: string, h // thunbnail, webpublic を必要なら生成 const alts = await generateAlts(path, type, !file.uri); - if (config.drive && config.drive.storage == 'minio') { + const meta = await fetchMeta(); + + if (meta.useObjectStorage) { //#region ObjectStorage params let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) || ['']); @@ -47,11 +48,11 @@ async function save(file: DriveFile, path: string, name: string, type: string, h if (type === 'image/webp') ext = '.webp'; } - const baseUrl = config.drive.baseUrl - || `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`; + const baseUrl = meta.objectStorageBaseUrl + || `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`; // for original - const key = `${config.drive.prefix}/${uuid.v4()}${ext}`; + const key = `${meta.objectStoragePrefix}/${uuid.v4()}${ext}`; const url = `${ baseUrl }/${ key }`; // for alts @@ -68,7 +69,7 @@ async function save(file: DriveFile, path: string, name: string, type: string, h ]; if (alts.webpublic) { - webpublicKey = `${config.drive.prefix}/${uuid.v4()}.${alts.webpublic.ext}`; + webpublicKey = `${meta.objectStoragePrefix}/${uuid.v4()}.${alts.webpublic.ext}`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`; logger.info(`uploading webpublic: ${webpublicKey}`); @@ -76,7 +77,7 @@ async function save(file: DriveFile, path: string, name: string, type: string, h } if (alts.thumbnail) { - thumbnailKey = `${config.drive.prefix}/${uuid.v4()}.${alts.thumbnail.ext}`; + thumbnailKey = `${meta.objectStoragePrefix}/${uuid.v4()}.${alts.thumbnail.ext}`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; logger.info(`uploading thumbnail: ${thumbnailKey}`); @@ -194,7 +195,15 @@ export async function generateAlts(path: string, type: string, generateWeb: bool * Upload to ObjectStorage */ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, filename?: string) { - const minio = new Minio.Client(config.drive!.config); + const meta = await fetchMeta(); + + const minio = new Minio.Client({ + endPoint: meta.objectStorageEndpoint!, + port: meta.objectStoragePort ? meta.objectStoragePort : undefined, + useSSL: meta.objectStorageUseSSL, + accessKey: meta.objectStorageAccessKey!, + secretKey: meta.objectStorageSecretKey!, + }); const metadata = { 'Content-Type': type, @@ -203,7 +212,7 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, if (filename) metadata['Content-Disposition'] = contentDisposition('inline', filename); - await minio.putObject(config.drive!.bucket!, key, stream, undefined, metadata); + await minio.putObject(meta.objectStorageBucket!, key, stream, undefined, metadata); } async function deleteOldFile(user: IRemoteUser) { diff --git a/src/services/drive/delete-file.ts b/src/services/drive/delete-file.ts index f1280822a..ba0482dbe 100644 --- a/src/services/drive/delete-file.ts +++ b/src/services/drive/delete-file.ts @@ -1,9 +1,9 @@ import * as Minio from 'minio'; -import config from '../../config'; import { DriveFile } from '../../models/entities/drive-file'; import { InternalStorage } from './internal-storage'; import { DriveFiles, Instances, Notes } from '../../models'; import { driveChart, perUserDriveChart, instanceChart } from '../chart'; +import { fetchMeta } from '../../misc/fetch-meta'; export default async function(file: DriveFile, isExpired = false) { if (file.storedInternal) { @@ -17,16 +17,24 @@ export default async function(file: DriveFile, isExpired = false) { InternalStorage.del(file.webpublicAccessKey!); } } else if (!file.isLink) { - const minio = new Minio.Client(config.drive!.config); + const meta = await fetchMeta(); - await minio.removeObject(config.drive!.bucket!, file.accessKey!); + const minio = new Minio.Client({ + endPoint: meta.objectStorageEndpoint!, + port: meta.objectStoragePort ? meta.objectStoragePort : undefined, + useSSL: meta.objectStorageUseSSL, + accessKey: meta.objectStorageAccessKey!, + secretKey: meta.objectStorageSecretKey!, + }); + + await minio.removeObject(meta.objectStorageBucket!, file.accessKey!); if (file.thumbnailUrl) { - await minio.removeObject(config.drive!.bucket!, file.thumbnailAccessKey!); + await minio.removeObject(meta.objectStorageBucket!, file.thumbnailAccessKey!); } if (file.webpublicUrl) { - await minio.removeObject(config.drive!.bucket!, file.webpublicAccessKey!); + await minio.removeObject(meta.objectStorageBucket!, file.webpublicAccessKey!); } } From 54bfffa7b98dfaee839a6a7beb59d7bbee6a07ff Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 16 May 2019 01:16:41 +0900 Subject: [PATCH 021/153] 11.14.0 --- CHANGELOG.md | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 795bef150..389fdf230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,20 @@ mongodb: 8. master ブランチに戻す 9. enjoy +11.14.0 (2019/05/16) +-------------------- +### 注意 +このバージョンからオブジェクトストレージの設定は設定ファイルではなく管理画面から行うようになりました。 +オブジェクトストレージを使用している場合、アップデートした後管理画面にアクセスしオブジェクトストレージの設定を再度行ってください。 + +### ✨Improvements +* 特定のユーザーのファイルをすべて削除できるように +* インスタンスの設定画面を整理 + +### 🐛Fixes +* GIF画像のサムネイルが生成されないのを修正 +* 管理画面の「ログ」で複数の除外条件を設定できない問題を修正 + 11.13.0 (2019/05/14) -------------------- ### 注意 diff --git a/package.json b/package.json index 5301118fd..8759edd4a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "misskey", "author": "syuilo ", - "version": "11.13.0", + "version": "11.14.0", "codename": "daybreak", "repository": { "type": "git", From 62cbb9215438ed9487588eeb74f1ae84c2e813d1 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 16 May 2019 12:59:09 +0900 Subject: [PATCH 022/153] Fix #4930 --- src/services/drive/add-file.ts | 1 + src/services/drive/delete-file.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index 701878b28..eb0b0abaa 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -199,6 +199,7 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, const minio = new Minio.Client({ endPoint: meta.objectStorageEndpoint!, + region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined, port: meta.objectStoragePort ? meta.objectStoragePort : undefined, useSSL: meta.objectStorageUseSSL, accessKey: meta.objectStorageAccessKey!, diff --git a/src/services/drive/delete-file.ts b/src/services/drive/delete-file.ts index ba0482dbe..b429ca0db 100644 --- a/src/services/drive/delete-file.ts +++ b/src/services/drive/delete-file.ts @@ -21,6 +21,7 @@ export default async function(file: DriveFile, isExpired = false) { const minio = new Minio.Client({ endPoint: meta.objectStorageEndpoint!, + region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined, port: meta.objectStoragePort ? meta.objectStoragePort : undefined, useSSL: meta.objectStorageUseSSL, accessKey: meta.objectStorageAccessKey!, From 183c82fb8db83cf9da69524d870385796d7632d2 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 16 May 2019 13:11:56 +0900 Subject: [PATCH 023/153] New Crowdin translations (#4924) * New translations ja-JP.yml (French) * New translations ja-JP.yml (French) * New translations ja-JP.yml (French) * New translations ja-JP.yml (French) * New translations ja-JP.yml (French) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Polish) * New translations ja-JP.yml (French) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Czech) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Spanish) * New translations ja-JP.yml (Japanese, Kansai) * New translations ja-JP.yml (English) * New translations ja-JP.yml (French) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Czech) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Japanese, Kansai) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Chinese Simplified) --- locales/cs-CZ.yml | 2 +- locales/en-US.yml | 2 +- locales/es-ES.yml | 1 - locales/fr-FR.yml | 26 +++++++++++++++++++++-- locales/ja-KS.yml | 2 +- locales/ko-KR.yml | 2 +- locales/pl-PL.yml | 1 - locales/zh-CN.yml | 54 ++++++++++++++++++++++++++++++++++++----------- 8 files changed, 70 insertions(+), 20 deletions(-) diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index aacbee9de..7b6327bc6 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -873,7 +873,6 @@ admin/views/index.vue: users: "Uživatelé" federation: "Z fedivesmíru" announcements: "Oznámení" - hashtags: "Hashtagy" queue: "Fronta úloh" logs: "Logy" back-to-misskey: "Zpět na Misskey" @@ -898,6 +897,7 @@ admin/views/instance.vue: maintainer-config: "Informace o administrátorovi" maintainer-name: "Jméno administrátora" maintainer-email: "Kontakt na administrátora" + object-storage-endpoint: "Endpoint" mb: "V megabajtech" recaptcha-config: "nastavení služby reCAPTCHA" recaptcha-info: "reCAPTCHA token je povinný. Můžete jej získat na https://www.google.com/recaptcha/intro/" diff --git a/locales/en-US.yml b/locales/en-US.yml index f32359306..530ce6ed8 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1063,7 +1063,6 @@ admin/views/index.vue: users: "Users" federation: "Federation" announcements: "Announcements" - hashtags: "Hashtags" abuse: "Abuse" queue: "Job Queue" logs: "Logs" @@ -1098,6 +1097,7 @@ admin/views/instance.vue: maintainer-name: "Administrator name" maintainer-email: "Contact Administrator" drive-config: "Drive settings" + object-storage-endpoint: "Endpoint" cache-remote-files: "Cache remote files" cache-remote-files-desc: "Without this parameter, all remote files are linked to their host server directly. This will be an effective solution to save your server storage, however make remote files invisible to users who set direct-link disabled, since no thumbnail will be generated, increase traffic. It is recommended that this parameter set enabled." local-drive-capacity-mb: "Volume of Drive per user" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 2cdc36fbe..f6e0b9f8b 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -910,7 +910,6 @@ admin/views/index.vue: moderators: "Moderadores" users: "Usuarios" federation: "Federado" - hashtags: "Hashtags" queue: "Cola de trabajos" logs: "Registros" back-to-misskey: "Volver a Misskey" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index eab827a77..29346854d 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -68,7 +68,7 @@ common: explore: "Découvrir" following: "Suit" followers: "Abonné·e·s" - favorites: "Mettre cette note en favoris" + favorites: "Favorites" permissions: "read:account": "Afficher les informations du compte" "write:account": "Mettre à jour les informations de votre compte" @@ -1046,7 +1046,6 @@ admin/views/index.vue: users: "Utilisateurs" federation: "Fédération" announcements: "Annonces" - hashtags: "Hashtags" abuse: "Abus" queue: "File d’attente" logs: "Journaux" @@ -1073,14 +1072,19 @@ admin/views/instance.vue: instance-name: "Nom de l’instance" instance-description: "Description de l’instance" host: "Hôte" + icon-url: "URL de l'icône" + logo-url: "URL do logo" banner-url: "URL de l’image de la bannière" error-image-url: "URL de l’image d’erreur" languages: "Langue de l’instance" languages-desc: "Vous pouvez en définir plus d’une, séparées par des espaces." + tos-url: "URL des conditions d'utilisation" + repository-url: "URL du dépôt" maintainer-config: "Informations de l’administrateur" maintainer-name: "Nom de l’administrateur" maintainer-email: "Contact administratif" drive-config: "Paramètres du lecteur" + object-storage-endpoint: "Point de terminaison" cache-remote-files: "Mettre en cache des fichiers distants" local-drive-capacity-mb: "Volume du lecteur par utilisateur" remote-drive-capacity-mb: "Volume du lecteur par utilisateur distant" @@ -1474,8 +1478,11 @@ mobile/views/components/ui.nav.vue: mobile/views/pages/drive.vue: contextmenu: upload: "Téléverser un fichier" + url-upload: "Transférer un fichier depuis une URL" create-folder: "Créer un dossier" rename-folder: "Renommer le dossier" + move-folder: "Déplacer ce dossier" + delete-folder: "Supprimer ce dossier" mobile/views/pages/user-lists.vue: title: "Listes" mobile/views/pages/signup.vue: @@ -1584,6 +1591,7 @@ dev/views/apps.vue: create-app: "Créer une app" app-missing: "Aucune application" dev/views/new-app.vue: + new-app: "Nouvelle application" create-app: "Création d’une application" app-name: "Nom de l’application" app-name-desc: "Le nom de votre application" @@ -1616,7 +1624,9 @@ pages: enter-variable-name: "Veuillez choisir un nom de variable" the-variable-name-is-already-used: "Cette variable est déjà utilisée" content-blocks: "Contenu du cadre" + input-blocks: "Entrée" special-blocks: "Spécial" + post-from-post-form: "Publier ce contenu" posted-from-post-form: "Publié !" blocks: text: "Texte" @@ -1653,11 +1663,15 @@ pages: _counter: name: "Nom de la variable" text: "Titre" + inc: "Augmenter le chiffre" _button: text: "Titre" + action: "L'opération lorsque le bouton sera pressé" _action: + dialog: "Afficher une fenêtre de dialogue" _dialog: content: "Contenu" + resetRandom: "Réinitialiser le nombre aléatoire" script: categories: flow: "Contrôle" @@ -1736,13 +1750,20 @@ pages: arg1: "Listes" _dailyRandomPick: arg1: "Listes" + _seedRannum: + arg2: "Min" + arg3: "Max" _seedRandomPick: arg2: "Listes" + pick: "Sélectionner dans la liste" _pick: arg1: "Listes" + arg2: "Position" number: "Numérique" + stringToNumber: "Chaîne en chiffres" _stringToNumber: arg1: "Texte" + numberToString: "Chiffres en chaîne" _numberToString: arg1: "Numérique" _splitStrByLine: @@ -1750,6 +1771,7 @@ pages: ref: "Variables" fn: "Fonction" _fn: + slots: "Emplacement" arg1: "Sortie" for: "Répéter" types: diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 224f0a40a..1610cad2d 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -836,7 +836,6 @@ admin/views/index.vue: users: "ユーザー" federation: "連合" announcements: "知っといてや" - hashtags: "ハッシュタグ" back-to-misskey: "Misskeyに戻る" admin/views/dashboard.vue: dashboard: "ダッシュボード" @@ -861,6 +860,7 @@ admin/views/instance.vue: maintainer-name: "管理者名" maintainer-email: "管理者の連絡先" drive-config: "ドライブの設定" + object-storage-endpoint: "エンドポイント" cache-remote-files: "リモートのファイルをキャッシュする" cache-remote-files-desc: "この設定を無効にすると、リモートファイルをこっちで保管せずに直接リンク張るようになるで。サーバーのストレージは軽くやろうけど、プライバシー設定で直リンクを向こうにしとるユーザーはファイルが見れへんし、サムネイルが無いから通信量が増えたりするから、普通はオンにしといてな。" local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 4434cf881..4a122c7d2 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1065,7 +1065,6 @@ admin/views/index.vue: users: "사용자" federation: "연합" announcements: "공지사항" - hashtags: "해시태그" abuse: "스팸 신고" queue: "작업 대기열" logs: "로그" @@ -1105,6 +1104,7 @@ admin/views/instance.vue: maintainer-name: "관리자 이름" maintainer-email: "관리자 연락처" drive-config: "드라이브 설정" + object-storage-endpoint: "엔드포인트" cache-remote-files: "원격 파일을 캐시" cache-remote-files-desc: "이 설정을 해지하면 원격 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 프라이버시 설정에서 직접 링크를 무효로 설정한 사용자에게는 파일이 보이지 않거나, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다. 일반적으로 이 설정을 ON으로 두는 것을 추천합니다." local-drive-capacity-mb: "로컬 사용자 한 명당 드라이브 용량" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 5d61fb5f6..827cc4fdf 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -855,7 +855,6 @@ admin/views/index.vue: moderators: "Moderatorzy" users: "Użytkownicy" announcements: "Ogłoszenia" - hashtags: "Hashtagi" admin/views/dashboard.vue: dashboard: "Kokpit" accounts: "Konta" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index d9ecd0d2a..07bec7354 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -145,7 +145,7 @@ common: profile: "个人资料" notification: "通知" apps: "应用程序" - tags: "标签" + tags: "哈希标签" mute-and-block: "屏蔽/拉黑" blocking: "拉黑" security: "安全性" @@ -168,7 +168,7 @@ common: use-avatar-reversi-stones: "用头像作为黑白棋的棋子" disable-animated-mfm: "在帖子中禁用动画文本" disable-showing-animated-images: "不播放动画" - suggest-recent-hashtags: "在帖子表单上显示最近流行的主题标签" + suggest-recent-hashtags: "在帖子表单上显示最近流行的哈希标签" always-show-nsfw: "总是显示 NSFW 的内容" always-mark-nsfw: "总是用 NSFW 来标记附件" show-full-acct: "不要从用户名中忽略主机名" @@ -297,7 +297,7 @@ common: server: "服务器信息" nav: "导航" tips: "提示" - hashtags: "标签" + hashtags: "哈希标签" queue: "队列" dev: "构建应用程序失败,请再试一次。" ai-chan-kawaii: "小蓝真可爱" @@ -469,9 +469,10 @@ common/views/components/nav.vue: status: "状态" wiki: "维基百科" donors: "捐赠者" - repository: "代码库" + repository: "源码库" develop: "开发人员" feedback: "反馈" + tos: "服务条款" common/views/components/note-menu.vue: mention: "提到" detail: "详细信息" @@ -584,6 +585,8 @@ common/views/components/signup.vue: password-matched: "确认" password-not-matched: "密码不一致" recaptcha: "验证" + agree-to: "同意{0}" + tos: "服务条款" create: "创建一个账户" some-error: "由于某种原因,创建帐户失败。请再试一次。" common/views/components/special-message.vue: @@ -713,7 +716,7 @@ common/views/widgets/posts-monitor.vue: title: "投稿表格" toggle: "切换视图" common/views/widgets/hashtags.vue: - title: "标签" + title: "哈希标签" common/views/widgets/server.vue: title: "服务器信息" toggle: "切换显示" @@ -1016,8 +1019,8 @@ desktop/views/components/timeline.vue: mentions: "提到的" messages: "直接发布" list: "列表" - hashtag: "标签" - add-tag-timeline: "添加标签" + hashtag: "哈希标签" + add-tag-timeline: "添加哈希标签" add-list: "添加列表" list-name: "列表名称" desktop/views/components/ui.header.vue: @@ -1063,7 +1066,6 @@ admin/views/index.vue: users: "用户" federation: "联合" announcements: "公告" - hashtags: "标签" abuse: "举报垃圾信息" queue: "作业队列" logs: "登录" @@ -1090,14 +1092,34 @@ admin/views/instance.vue: instance-name: "实例名称" instance-description: "实例介绍" host: "主机名" + icon-url: "图标URL" + logo-url: "Logo URL" banner-url: "背景图片地址" error-image-url: "无效的图像URL" languages: "实例语言" languages-desc: "您可以添加多个,以空格分隔。" + tos-url: "服务条款URL" + repository-url: "源码库URL" + feedback-url: "反馈URL" maintainer-config: "管理员信息" maintainer-name: "管理员名称" maintainer-email: "联系管理员" + advanced-config: "其他设置" + note-and-tl: "帖子和时间线" drive-config: "网盘设置" + use-object-storage: "使用对象存储" + object-storage-base-url: "URL" + object-storage-bucket: "存储空间名" + object-storage-prefix: "前缀" + object-storage-endpoint: "端点" + object-storage-region: "区域" + object-storage-port: "端口" + object-storage-access-key: "访问密钥" + object-storage-secret-key: "密钥" + object-storage-use-ssl: "使用 SSL" + object-storage-s3-info: "使用Amazon S3作为对象存储时,请确认{0}相关“终端”和“区域”的设置。" + object-storage-s3-info-here: "这里" + object-storage-gcs-info: "将Google Cloud Storage用作对象存储时,请将“终端”设置为storage.googleapis.com,并将“区域”留空。" cache-remote-files: "远程文件缓存" cache-remote-files-desc: "如果没有此参数,则所有远程文件都将直接链接到其主机服务器。 这将是保存服务器存储的有效解决方案,但是对于设置禁用直接链接的用户而言,远程文件不可见,因为不会生成缩略图,从而增加流量。 建议启用此参数集。" local-drive-capacity-mb: "每个用户的网盘空间" @@ -1108,6 +1130,9 @@ admin/views/instance.vue: enable-recaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)" recaptcha-site-key: "reCAPTCHA site key" recaptcha-secret-key: "reCAPTCHA secret key" + hidden-tags: "隐藏哈希标签" + hidden-tags-info: "使用换行符分隔要从集合中排除的哈希标签。" + external-service-integration-config: "连接外部服务" twitter-integration-config: "连接到Twitter的设置" twitter-integration-info: "设置返回的URL{url}。" enable-twitter-integration: "启用连接到Twitter" @@ -1139,6 +1164,7 @@ admin/views/instance.vue: save: "保存" saved: "保存完毕" pinned-users: "置顶用户" + pinned-users-info: "描述您要置顶的用户,以换行符分隔。" email-config: "电子邮件服务器设置" email-config-info: "用于确认电子邮件和密码重置等。" enable-email: "启用电子邮件送递" @@ -1224,6 +1250,8 @@ admin/views/users.vue: unsilence-confirm: "解除屏蔽?" update-remote-user: "更新远程用户信息" remote-user-updated: "远程用户信息已更新" + delete-all-files: "删除所有文件" + delete-all-files-confirm: "删除所有文件吗?" users: title: "用户" sort: @@ -1299,6 +1327,7 @@ admin/views/federation.vue: latest-request-received-at: "上次收到的请求" remove-all-following: "取消所有关注" remove-all-following-info: "取消{host}的所有关注者。当实例不存在时执行。" + delete-all-files: "删除所有文件" block: "拉黑" marked-as-closed: "标记为已关闭" lookup: "查询" @@ -1346,6 +1375,7 @@ admin/views/federation.vue: hour: "每小时" day: "每天" blocked-hosts: "拉黑" + blocked-hosts-info: "描述您要阻止的主机,以换行符分隔。" desktop/views/pages/welcome.vue: about: "更多信息..." timeline: "时间线" @@ -1367,7 +1397,7 @@ desktop/views/pages/search.vue: not-available: "在此实例的设置中关闭搜索功能。" not-found: "没有找到“{q}”的帖子" desktop/views/pages/tag.vue: - no-posts-found: "没有找到带有主题标签“{q}”的帖子" + no-posts-found: "没有找到带有哈希标签“{q}”的帖子" desktop/views/pages/user-list.users.vue: users: "用户" add-user: "添加用户" @@ -1443,7 +1473,7 @@ mobile/views/components/drive.file-detail.vue: download: "下载" rename: "重命名" move: "移动" - hash: "Hash (md5)" + hash: "哈希(md5)" exif: "EXIF" nsfw: "阅读注意" mark-as-sensitive: "标记为“敏感”" @@ -1530,7 +1560,7 @@ mobile/views/pages/home.vue: mentions: "Mentions" messages: "直接发布" mobile/views/pages/tag.vue: - no-posts-found: "没有找到带有主题标签“{q}”的帖子" + no-posts-found: "没有找到带有哈希标签“{q}”的帖子" mobile/views/pages/widgets.vue: dashboard: "仪表盘" widgets-hints: "您可以添加/删除/重新排列小部件。 要移动小部件,请拖动“三”。 点击“×”删除小部件。 某些小部件可以通过点击来更改显示。" @@ -1582,7 +1612,7 @@ deck: home: "首页" local: "Local" hybrid: "社交" - hashtag: "标签" + hashtag: "哈希标签" global: "Global" mentions: "Mentions" direct: "直接发布" From 70d710c9a9199c4ab017de065ec6e69400afc22a Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 16 May 2019 16:08:50 +0900 Subject: [PATCH 024/153] =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E3=81=A7reCAPTCHA=E3=81=AE=E3=83=97=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=99=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ja-JP.yml | 5 ++-- src/client/app/admin/views/instance.vue | 32 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index bb991459c..f34b01563 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1253,8 +1253,9 @@ admin/views/instance.vue: recaptcha-config: "reCAPTCHAの設定" recaptcha-info: "reCAPTCHAを有効にする場合、reCAPTCHAトークンを取得する必要があります。https://www.google.com/recaptcha/intro/ にアクセスしてトークンを取得してください。" enable-recaptcha: "reCAPTCHAを有効にする" - recaptcha-site-key: "reCAPTCHA site key" - recaptcha-secret-key: "reCAPTCHA secret key" + recaptcha-site-key: "サイトキー" + recaptcha-secret-key: "シークレットキー" + recaptcha-preview: "プレビュー" hidden-tags: "非表示ハッシュタグ" hidden-tags-info: "集計から除外するハッシュタグを改行で区切って記述します。" external-service-integration-config: "外部サービス連携" diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue index 3ac4d6d72..72ae3384b 100644 --- a/src/client/app/admin/views/instance.vue +++ b/src/client/app/admin/views/instance.vue @@ -165,6 +165,10 @@
    +
    +
    {{ $t('recaptcha-preview') }}
    +
    +
    {{ $t('save') }}
    @@ -376,6 +380,34 @@ export default Vue.extend({ }); }, + mounted() { + const renderRecaptchaPreview = () => { + if (!(window as any).grecaptcha) return; + if (!this.$refs.recaptcha) return; + if (!this.recaptchaSiteKey) return; + (window as any).grecaptcha.render(this.$refs.recaptcha, { + sitekey: this.recaptchaSiteKey + }); + }; + + window.onRecaotchaLoad = () => { + renderRecaptchaPreview(); + }; + + const head = document.getElementsByTagName('head')[0]; + const script = document.createElement('script'); + script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad'); + head.appendChild(script); + + this.$watch('enableRecaptcha', () => { + renderRecaptchaPreview(); + }); + + this.$watch('recaptchaSiteKey', () => { + renderRecaptchaPreview(); + }); + }, + methods: { invite() { this.$root.api('admin/invite').then(x => { From a21357248f83703acaba68e6adf5c9b5695da749 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 16 May 2019 16:12:30 +0900 Subject: [PATCH 025/153] =?UTF-8?q?Docker=E3=82=92=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E3=81=97=E3=81=A6=E3=81=84=E3=82=8B=E5=A0=B4=E5=90=88=E3=80=81?= =?UTF-8?q?=E3=82=A2=E3=83=97=E3=83=87=E3=81=AE=E9=9A=9B=E3=81=AB=E3=83=9E?= =?UTF-8?q?=E3=82=A4=E3=82=B0=E3=83=AC=E3=82=92=E8=87=AA=E5=8B=95=E5=AE=9F?= =?UTF-8?q?=E8=A1=8C=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- package.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8a6150876..e5e6c06a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,4 +39,4 @@ COPY --from=builder /misskey/node_modules ./node_modules COPY --from=builder /misskey/built ./built COPY . ./ -CMD ["npm", "start"] +CMD ["npm", "migrateandstart"] diff --git a/package.json b/package.json index 8759edd4a..40f2bdd6a 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "init": "node ./built/init.js", "ormconfig": "node ./built/ormconfig.js", "migrate": "npm run ormconfig && ts-node ./node_modules/typeorm/cli.js migration:run", + "migrateandstart": "npm run migrate && npm run start", "build": "webpack && gulp build", "webpack": "webpack", "watch": "webpack --watch", From fe6d88e4101e7ffe569deb3397a24e49824aaa4a Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 16 May 2019 16:13:46 +0900 Subject: [PATCH 026/153] 11.15.0 --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 389fdf230..af329d9ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,14 @@ mongodb: 8. master ブランチに戻す 9. enjoy +11.15.0 (2019/05/16) +-------------------- +### ✨Improvements +* 管理画面でreCAPTCHAのプレビューを表示するように + +### 🐛Fixes +* オブジェクトストレージのリージョンの設定が反映されない問題を修正 + 11.14.0 (2019/05/16) -------------------- ### 注意 diff --git a/package.json b/package.json index 40f2bdd6a..84a6436ec 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "misskey", "author": "syuilo ", - "version": "11.14.0", + "version": "11.15.0", "codename": "daybreak", "repository": { "type": "git", From d6ccb1725bd32bb7e6c35430b7631c3632d87252 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 17 May 2019 00:19:23 +0900 Subject: [PATCH 027/153] Update API docs --- src/server/api/endpoints/notes/favorites/create.ts | 2 +- src/server/api/endpoints/notes/favorites/delete.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts index bb0c9594b..e3a786fdb 100644 --- a/src/server/api/endpoints/notes/favorites/create.ts +++ b/src/server/api/endpoints/notes/favorites/create.ts @@ -14,7 +14,7 @@ export const meta = { 'en-US': 'Favorite a note.' }, - tags: ['favorites'], + tags: ['notes', 'favorites'], requireCredential: true, diff --git a/src/server/api/endpoints/notes/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts index 49f763177..eea35ef58 100644 --- a/src/server/api/endpoints/notes/favorites/delete.ts +++ b/src/server/api/endpoints/notes/favorites/delete.ts @@ -13,7 +13,7 @@ export const meta = { 'en-US': 'Unfavorite a note.' }, - tags: ['favorites'], + tags: ['notes', 'favorites'], requireCredential: true, From 81625f9fc52be901d3e80b5aebe3e5106448eb5e Mon Sep 17 00:00:00 2001 From: ql3 <49830167+ql3@users.noreply.github.com> Date: Fri, 17 May 2019 02:31:00 +0900 Subject: [PATCH 028/153] Update gitignore to ignore config files for Intelij-IDEA (#4933) --- .gitignore | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 255b1ad4d..5d06997f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,23 @@ +# Visual Studio Code +/.vscode + +# Intelij-IDEA +/.idea + +# Node.js +/node_modules + +# yarn +yarn.lock + +# config /.config/* !/.config/example.yml !/.config/mongo_initdb_example.js -/.vscode -/node_modules + +# misskey /build /built -built /data /.cache-loader /db @@ -17,7 +29,6 @@ api-docs.json *.log /redis *.code-workspace -yarn.lock .DS_Store /files ormconfig.json From 380749051d1bdb63c667dd055f949f339c356e35 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 17 May 2019 19:56:47 +0900 Subject: [PATCH 029/153] =?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AB?= =?UTF-8?q?=E3=81=84=E3=81=84=E3=81=AD=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ja-JP.yml | 4 + migration/1558072954435-PageLike.ts | 23 +++ .../app/common/views/pages/page/page.vue | 32 +++- src/client/app/common/views/pages/pages.vue | 138 ++++++++++++++++++ src/client/app/desktop/script.ts | 2 +- src/client/app/desktop/views/home/pages.vue | 92 ------------ src/client/app/mobile/views/pages/pages.vue | 79 +--------- src/db/postgre.ts | 2 + src/models/entities/page-like.ts | 33 +++++ src/models/entities/page.ts | 5 + src/models/index.ts | 2 + src/models/repositories/page-like.ts | 26 ++++ src/models/repositories/page.ts | 52 ++++--- src/server/api/endpoints/i/page-likes.ts | 45 ++++++ src/server/api/endpoints/pages/like.ts | 79 ++++++++++ src/server/api/endpoints/pages/show.ts | 2 +- src/server/api/endpoints/pages/unlike.ts | 62 ++++++++ src/server/api/kinds.ts | 2 + 18 files changed, 489 insertions(+), 191 deletions(-) create mode 100644 migration/1558072954435-PageLike.ts create mode 100644 src/client/app/common/views/pages/pages.vue delete mode 100644 src/client/app/desktop/views/home/pages.vue create mode 100644 src/models/entities/page-like.ts create mode 100644 src/models/repositories/page-like.ts create mode 100644 src/server/api/endpoints/i/page-likes.ts create mode 100644 src/server/api/endpoints/pages/like.ts create mode 100644 src/server/api/endpoints/pages/unlike.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f34b01563..dc0692e4b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1874,6 +1874,10 @@ pages: edit-this-page: "このページを編集" view-source: "ソースを表示" view-page: "ページを見る" + like: "いいね" + unlike: "いいね解除" + liked-pages: "いいねしたページ" + my-pages: "自分のページ" inspector: "インスペクター" content: "ページブロック" variables: "変数" diff --git a/migration/1558072954435-PageLike.ts b/migration/1558072954435-PageLike.ts new file mode 100644 index 000000000..93cdb8afe --- /dev/null +++ b/migration/1558072954435-PageLike.ts @@ -0,0 +1,23 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class PageLike1558072954435 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "page_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "pageId" character varying(32) NOT NULL, CONSTRAINT "PK_813f034843af992d3ae0f43c64c" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_0e61efab7f88dbb79c9166dbb4" ON "page_like" ("userId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4ce6fb9c70529b4c8ac46c9bfa" ON "page_like" ("userId", "pageId") `); + await queryRunner.query(`ALTER TABLE "page" ADD "likedCount" integer NOT NULL DEFAULT 0`); + await queryRunner.query(`ALTER TABLE "page_like" ADD CONSTRAINT "FK_0e61efab7f88dbb79c9166dbb48" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "page_like" ADD CONSTRAINT "FK_cf8782626dced3176038176a847" FOREIGN KEY ("pageId") REFERENCES "page"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "page_like" DROP CONSTRAINT "FK_cf8782626dced3176038176a847"`); + await queryRunner.query(`ALTER TABLE "page_like" DROP CONSTRAINT "FK_0e61efab7f88dbb79c9166dbb48"`); + await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "likedCount"`); + await queryRunner.query(`DROP INDEX "IDX_4ce6fb9c70529b4c8ac46c9bfa"`); + await queryRunner.query(`DROP INDEX "IDX_0e61efab7f88dbb79c9166dbb4"`); + await queryRunner.query(`DROP TABLE "page_like"`); + } + +} diff --git a/src/client/app/common/views/pages/page/page.vue b/src/client/app/common/views/pages/page/page.vue index 29580fab6..d3fb948c8 100644 --- a/src/client/app/common/views/pages/page/page.vue +++ b/src/client/app/common/views/pages/page/page.vue @@ -12,6 +12,11 @@ @{{ page.user.username }} {{ $t('edit-this-page') }} {{ $t('view-source') }} +
    @@ -19,8 +24,8 @@ @@ -161,4 +184,7 @@ export default Vue.extend({ > a + a margin-left 8px + > .like + margin-top 16px + diff --git a/src/client/app/common/views/pages/pages.vue b/src/client/app/common/views/pages/pages.vue new file mode 100644 index 000000000..751ea7237 --- /dev/null +++ b/src/client/app/common/views/pages/pages.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index e8da23526..464f7d3ce 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -156,7 +156,7 @@ init(async (launch, os) => { { path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, { path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, { path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) }, - { path: '/i/pages', component: () => import('./views/home/pages.vue').then(m => m.default) }, + { path: '/i/pages', component: () => import('../common/views/pages/pages.vue').then(m => m.default) }, ]}, { path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) }, { path: '/@:user/pages/:pageName/view-source', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, diff --git a/src/client/app/desktop/views/home/pages.vue b/src/client/app/desktop/views/home/pages.vue deleted file mode 100644 index 9f7fb6515..000000000 --- a/src/client/app/desktop/views/home/pages.vue +++ /dev/null @@ -1,92 +0,0 @@ - - - - - diff --git a/src/client/app/mobile/views/pages/pages.vue b/src/client/app/mobile/views/pages/pages.vue index 100c814ad..2fd134fcd 100644 --- a/src/client/app/mobile/views/pages/pages.vue +++ b/src/client/app/mobile/views/pages/pages.vue @@ -3,92 +3,27 @@
    - - - - - {{ $t('@.load-more') }} +
    + - - diff --git a/src/db/postgre.ts b/src/db/postgre.ts index 18283836a..f488af03c 100644 --- a/src/db/postgre.ts +++ b/src/db/postgre.ts @@ -41,6 +41,7 @@ import { UserKeypair } from '../models/entities/user-keypair'; import { UserPublickey } from '../models/entities/user-publickey'; import { UserProfile } from '../models/entities/user-profile'; import { Page } from '../models/entities/page'; +import { PageLike } from '../models/entities/page-like'; const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); @@ -116,6 +117,7 @@ export function initDb(justBorrow = false, sync = false, log = false) { NoteWatching, NoteUnread, Page, + PageLike, Log, DriveFile, DriveFolder, diff --git a/src/models/entities/page-like.ts b/src/models/entities/page-like.ts new file mode 100644 index 000000000..ca84ece8f --- /dev/null +++ b/src/models/entities/page-like.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; +import { Page } from './page'; + +@Entity() +@Index(['userId', 'pageId'], { unique: true }) +export class PageLike { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public pageId: Page['id']; + + @ManyToOne(type => Page, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public page: Page | null; +} diff --git a/src/models/entities/page.ts b/src/models/entities/page.ts index f57ca8c7c..05015ba17 100644 --- a/src/models/entities/page.ts +++ b/src/models/entities/page.ts @@ -95,6 +95,11 @@ export class Page { }) public visibleUserIds: User['id'][]; + @Column('integer', { + default: 0 + }) + public likedCount: number; + constructor(data: Partial) { if (data == null) return; diff --git a/src/models/index.ts b/src/models/index.ts index e402d6723..a63bb2c2b 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -36,6 +36,7 @@ import { AuthSessionRepository } from './repositories/auth-session'; import { UserProfile } from './entities/user-profile'; import { HashtagRepository } from './repositories/hashtag'; import { PageRepository } from './repositories/page'; +import { PageLikeRepository } from './repositories/page-like'; export const Apps = getCustomRepository(AppRepository); export const Notes = getCustomRepository(NoteRepository); @@ -74,3 +75,4 @@ export const ReversiGames = getCustomRepository(ReversiGameRepository); export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository); export const Logs = getRepository(Log); export const Pages = getCustomRepository(PageRepository); +export const PageLikes = getCustomRepository(PageLikeRepository); diff --git a/src/models/repositories/page-like.ts b/src/models/repositories/page-like.ts new file mode 100644 index 000000000..3e7e803fd --- /dev/null +++ b/src/models/repositories/page-like.ts @@ -0,0 +1,26 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { PageLike } from '../entities/page-like'; +import { Pages } from '..'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(PageLike) +export class PageLikeRepository extends Repository { + public async pack( + src: PageLike['id'] | PageLike, + me?: any + ) { + const like = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return { + id: like.id, + page: await Pages.pack(like.page || like.pageId, me), + }; + } + + public packMany( + likes: any[], + me: any + ) { + return Promise.all(likes.map(x => this.pack(x, me))); + } +} diff --git a/src/models/repositories/page.ts b/src/models/repositories/page.ts index 2293edbc0..3b4142002 100644 --- a/src/models/repositories/page.ts +++ b/src/models/repositories/page.ts @@ -1,24 +1,30 @@ import { EntityRepository, Repository } from 'typeorm'; import { Page } from '../entities/page'; import { SchemaType, types, bool } from '../../misc/schema'; -import { Users, DriveFiles } from '..'; +import { Users, DriveFiles, PageLikes } from '..'; import { awaitAll } from '../../prelude/await-all'; import { DriveFile } from '../entities/drive-file'; +import { User } from '../entities/user'; +import { ensure } from '../../prelude/ensure'; export type PackedPage = SchemaType; @EntityRepository(Page) export class PageRepository extends Repository { public async pack( - src: Page, + src: Page['id'] | Page, + me?: User['id'] | User | null | undefined, ): Promise { + const meId = me ? typeof me === 'string' ? me : me.id : null; + const page = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + const attachedFiles: Promise[] = []; const collectFile = (xs: any[]) => { for (const x of xs) { if (x.type === 'image') { attachedFiles.push(DriveFiles.findOne({ id: x.fileId, - userId: src.userId + userId: page.userId })); } if (x.children) { @@ -26,7 +32,7 @@ export class PageRepository extends Repository { } } }; - collectFile(src.content); + collectFile(page.content); // 後方互換性のため let migrated = false; @@ -47,29 +53,31 @@ export class PageRepository extends Repository { } } }; - migrate(src.content); + migrate(page.content); if (migrated) { - this.update(src.id, { - content: src.content + this.update(page.id, { + content: page.content }); } return await awaitAll({ - id: src.id, - createdAt: src.createdAt.toISOString(), - updatedAt: src.updatedAt.toISOString(), - userId: src.userId, - user: Users.pack(src.user || src.userId), - content: src.content, - variables: src.variables, - title: src.title, - name: src.name, - summary: src.summary, - alignCenter: src.alignCenter, - font: src.font, - eyeCatchingImageId: src.eyeCatchingImageId, - eyeCatchingImage: src.eyeCatchingImageId ? await DriveFiles.pack(src.eyeCatchingImageId) : null, - attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)) + id: page.id, + createdAt: page.createdAt.toISOString(), + updatedAt: page.updatedAt.toISOString(), + userId: page.userId, + user: Users.pack(page.user || page.userId), + content: page.content, + variables: page.variables, + title: page.title, + name: page.name, + summary: page.summary, + alignCenter: page.alignCenter, + font: page.font, + eyeCatchingImageId: page.eyeCatchingImageId, + eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null, + attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)), + likedCount: page.likedCount, + isLiked: meId ? await PageLikes.findOne({ pageId: page.id, userId: meId }).then(x => x != null) : undefined, }); } diff --git a/src/server/api/endpoints/i/page-likes.ts b/src/server/api/endpoints/i/page-likes.ts new file mode 100644 index 000000000..23bde74c9 --- /dev/null +++ b/src/server/api/endpoints/i/page-likes.ts @@ -0,0 +1,45 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { PageLikes } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; + +export const meta = { + desc: { + 'ja-JP': '「いいね」したページ一覧を取得します。', + 'en-US': 'Get liked pages' + }, + + tags: ['account', 'pages'], + + requireCredential: true, + + kind: 'read:page-likes', + + params: { + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + } +}; + +export default define(meta, async (ps, user) => { + const query = makePaginationQuery(PageLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId) + .andWhere(`like.userId = :meId`, { meId: user.id }) + .leftJoinAndSelect('like.page', 'page'); + + const likes = await query + .take(ps.limit!) + .getMany(); + + return await PageLikes.packMany(likes, user); +}); diff --git a/src/server/api/endpoints/pages/like.ts b/src/server/api/endpoints/pages/like.ts new file mode 100644 index 000000000..5a50bd6c6 --- /dev/null +++ b/src/server/api/endpoints/pages/like.ts @@ -0,0 +1,79 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Pages, PageLikes } from '../../../../models'; +import { genId } from '../../../../misc/gen-id'; + +export const meta = { + desc: { + 'ja-JP': '指定したページを「いいね」します。', + }, + + tags: ['pages'], + + requireCredential: true, + + kind: 'write:page-likes', + + params: { + pageId: { + validator: $.type(ID), + desc: { + 'ja-JP': '対象のページのID', + 'en-US': 'Target page ID.' + } + } + }, + + errors: { + noSuchPage: { + message: 'No such page.', + code: 'NO_SUCH_PAGE', + id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3' + }, + + yourPage: { + message: 'You cannot like your page.', + code: 'YOUR_PAGE', + id: '28800466-e6db-40f2-8fae-bf9e82aa92b8' + }, + + alreadyLiked: { + message: 'The page has already been liked.', + code: 'ALREADY_LIKED', + id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3' + }, + } +}; + +export default define(meta, async (ps, user) => { + const page = await Pages.findOne(ps.pageId); + if (page == null) { + throw new ApiError(meta.errors.noSuchPage); + } + + if (page.userId === user.id) { + throw new ApiError(meta.errors.yourPage); + } + + // if already liked + const exist = await PageLikes.findOne({ + pageId: page.id, + userId: user.id + }); + + if (exist != null) { + throw new ApiError(meta.errors.alreadyLiked); + } + + // Create like + await PageLikes.save({ + id: genId(), + createdAt: new Date(), + pageId: page.id, + userId: user.id + }); + + Pages.increment({ id: page.id }, 'likedCount', 1); +}); diff --git a/src/server/api/endpoints/pages/show.ts b/src/server/api/endpoints/pages/show.ts index dd1dc9f25..e3d6e6a15 100644 --- a/src/server/api/endpoints/pages/show.ts +++ b/src/server/api/endpoints/pages/show.ts @@ -70,5 +70,5 @@ export default define(meta, async (ps, user) => { throw new ApiError(meta.errors.noSuchPage); } - return await Pages.pack(page); + return await Pages.pack(page, user); }); diff --git a/src/server/api/endpoints/pages/unlike.ts b/src/server/api/endpoints/pages/unlike.ts new file mode 100644 index 000000000..49ad999b3 --- /dev/null +++ b/src/server/api/endpoints/pages/unlike.ts @@ -0,0 +1,62 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Pages, PageLikes } from '../../../../models'; + +export const meta = { + desc: { + 'ja-JP': '指定したページの「いいね」を解除します。', + }, + + tags: ['pages'], + + requireCredential: true, + + kind: 'write:page-likes', + + params: { + pageId: { + validator: $.type(ID), + desc: { + 'ja-JP': '対象のページのID', + 'en-US': 'Target page ID.' + } + } + }, + + errors: { + noSuchPage: { + message: 'No such page.', + code: 'NO_SUCH_PAGE', + id: 'a0d41e20-1993-40bd-890e-f6e560ae648e' + }, + + notLiked: { + message: 'You have not liked that page.', + code: 'NOT_LIKED', + id: 'f5e586b0-ce93-4050-b0e3-7f31af5259ee' + }, + } +}; + +export default define(meta, async (ps, user) => { + const page = await Pages.findOne(ps.pageId); + if (page == null) { + throw new ApiError(meta.errors.noSuchPage); + } + + const exist = await PageLikes.findOne({ + pageId: page.id, + userId: user.id + }); + + if (exist == null) { + throw new ApiError(meta.errors.notLiked); + } + + // Delete like + await PageLikes.delete(exist.id); + + Pages.decrement({ id: page.id }, 'likedCount', 1); +}); diff --git a/src/server/api/kinds.ts b/src/server/api/kinds.ts index 99c379558..76d5a8a61 100644 --- a/src/server/api/kinds.ts +++ b/src/server/api/kinds.ts @@ -21,4 +21,6 @@ export const kinds = [ 'write:votes', 'read:pages', 'write:pages', + 'write:page-likes', + 'read:page-likes', ]; From 61f54f8f749ffbc6c376ff7c96d0135ba8417b8f Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 18 May 2019 00:38:33 +0900 Subject: [PATCH 030/153] Fix bug --- src/services/update-hashtag.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/update-hashtag.ts b/src/services/update-hashtag.ts index 8dbbf04cb..3482b9ef0 100644 --- a/src/services/update-hashtag.ts +++ b/src/services/update-hashtag.ts @@ -64,6 +64,7 @@ export async function updateHashtag(user: User, tag: string, isUserAttached = fa } if (Object.keys(set).length > 0) { + q.set(set); q.execute(); } } else { From c7cc3dcdfd2c0962a39e7186852a17dbd09b6a5b Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 18 May 2019 20:36:33 +0900 Subject: [PATCH 031/153] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E3=82=B0=E3=83=AB=E3=83=BC=E3=83=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #3218 --- locales/ja-JP.yml | 48 +++-- migration/1558103093633-UserGroup.ts | 41 ++++ .../app/common/views/components/dialog.vue | 1 + .../app/common/views/components/index.ts | 4 + .../views/components/messaging-room.form.vue | 16 +- .../components/messaging-room.message.vue | 10 +- .../views/components/messaging-room.vue | 56 ++++-- .../app/common/views/components/messaging.vue | 141 +++++++++++--- .../app/common/views/components/ui/hr.vue | 15 ++ .../app/common/views/components/ui/margin.vue | 16 ++ .../common/views/components/user-lists.vue | 95 --------- .../app/common/views/components/user-menu.vue | 2 +- ...re-column.vue => deck.column-template.vue} | 29 ++- src/client/app/common/views/pages/explore.vue | 4 + .../views/pages/follow-requests.vue} | 39 ++-- src/client/app/common/views/pages/pages.vue | 5 + .../common/views/pages/user-group-editor.vue | 180 ++++++++++++++++++ .../app/common/views/pages/user-groups.vue | 63 ++++++ .../user-list-editor.vue | 60 ++++-- .../app/common/views/pages/user-lists.vue | 63 ++++++ src/client/app/desktop/script.ts | 18 +- .../components/messaging-room-window.vue | 12 +- .../views/components/messaging-window.vue | 7 +- .../received-follow-requests-window.vue | 70 ------- .../views/components/ui.header.account.vue | 33 ++-- .../desktop/views/components/ui.sidebar.vue | 2 - .../views/components/user-list-window.vue | 24 --- .../views/components/user-lists-window.vue | 36 ---- .../desktop/views/pages/messaging-room.vue | 28 ++- .../app/desktop/views/widgets/messaging.vue | 7 +- src/client/app/mobile/script.ts | 31 +-- .../app/mobile/views/components/ui.nav.vue | 2 +- src/client/app/mobile/views/pages/explore.vue | 28 --- .../app/mobile/views/pages/messaging-room.vue | 23 ++- .../app/mobile/views/pages/messaging.vue | 5 +- src/client/app/mobile/views/pages/pages.vue | 29 --- src/client/app/mobile/views/pages/ui.vue | 38 ++++ .../app/mobile/views/pages/user-list.vue | 48 ----- .../app/mobile/views/pages/user-lists.vue | 35 ---- src/db/postgre.ts | 4 + src/models/entities/messaging-message.ts | 24 ++- src/models/entities/user-group-joining.ts | 41 ++++ src/models/entities/user-group.ts | 46 +++++ src/models/index.ts | 6 +- src/models/repositories/messaging-message.ts | 38 +++- src/models/repositories/user-group.ts | 61 ++++++ src/models/repositories/user.ts | 35 +++- .../api/common/read-messaging-message.ts | 84 ++++++-- src/server/api/endpoints/messaging/history.ts | 53 ++++-- .../api/endpoints/messaging/messages.ts | 105 ++++++++-- .../endpoints/messaging/messages/create.ts | 125 +++++++++--- .../endpoints/messaging/messages/delete.ts | 12 +- .../api/endpoints/messaging/messages/read.ts | 17 +- .../api/endpoints/users/groups/create.ts | 51 +++++ .../api/endpoints/users/groups/delete.ts | 49 +++++ .../api/endpoints/users/groups/joined.ts | 33 ++++ .../api/endpoints/users/groups/owned.ts | 33 ++++ src/server/api/endpoints/users/groups/pull.ts | 68 +++++++ src/server/api/endpoints/users/groups/push.ts | 90 +++++++++ src/server/api/endpoints/users/groups/show.ts | 53 ++++++ src/server/api/endpoints/users/lists/push.ts | 2 +- src/server/api/kinds.ts | 2 + src/server/api/openapi/schemas.ts | 2 + src/server/api/stream/channels/messaging.ts | 31 ++- src/services/stream.ts | 6 + 65 files changed, 1797 insertions(+), 638 deletions(-) create mode 100644 migration/1558103093633-UserGroup.ts create mode 100644 src/client/app/common/views/components/ui/hr.vue create mode 100644 src/client/app/common/views/components/ui/margin.vue delete mode 100644 src/client/app/common/views/components/user-lists.vue rename src/client/app/common/views/deck/{deck.explore-column.vue => deck.column-template.vue} (50%) rename src/client/app/{mobile/views/pages/received-follow-requests.vue => common/views/pages/follow-requests.vue} (57%) create mode 100644 src/client/app/common/views/pages/user-group-editor.vue create mode 100644 src/client/app/common/views/pages/user-groups.vue rename src/client/app/common/views/{components => pages}/user-list-editor.vue (66%) create mode 100644 src/client/app/common/views/pages/user-lists.vue delete mode 100644 src/client/app/desktop/views/components/received-follow-requests-window.vue delete mode 100644 src/client/app/desktop/views/components/user-list-window.vue delete mode 100644 src/client/app/desktop/views/components/user-lists-window.vue delete mode 100644 src/client/app/mobile/views/pages/explore.vue delete mode 100644 src/client/app/mobile/views/pages/pages.vue create mode 100644 src/client/app/mobile/views/pages/ui.vue delete mode 100644 src/client/app/mobile/views/pages/user-list.vue delete mode 100644 src/client/app/mobile/views/pages/user-lists.vue create mode 100644 src/models/entities/user-group-joining.ts create mode 100644 src/models/entities/user-group.ts create mode 100644 src/models/repositories/user-group.ts create mode 100644 src/server/api/endpoints/users/groups/create.ts create mode 100644 src/server/api/endpoints/users/groups/delete.ts create mode 100644 src/server/api/endpoints/users/groups/joined.ts create mode 100644 src/server/api/endpoints/users/groups/owned.ts create mode 100644 src/server/api/endpoints/users/groups/pull.ts create mode 100644 src/server/api/endpoints/users/groups/push.ts create mode 100644 src/server/api/endpoints/users/groups/show.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index dc0692e4b..437fd3997 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -265,6 +265,7 @@ common: my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。" hide-password: "パスワードを隠す" show-password: "パスワードを表示する" + enter-username: "ユーザー名を入力してください" do-not-use-in-production: "これは開発ビルドです。本番環境で使用しないでください。" user-suspended: "このユーザーは凍結されています。" @@ -480,20 +481,24 @@ common/views/components/messaging.vue: search-user: "ユーザーを探す" you: "あなた" no-history: "履歴はありません" + user: "ユーザー" + group: "グループ" + start-with-user: "ユーザーとトークを開始" + start-with-group: "グループとトークを開始" common/views/components/messaging-room.vue: - empty: "このユーザーと話したことはありません" + not-talked-user: "このユーザーとの会話はありません" + not-talked-group: "このグループでの会話はありません" no-history: "これより過去の履歴はありません" - resize-form: "ドラッグしてフォームの広さを調整" new-message: "新しいメッセージがあります" - only-one-file-attached: "メッセージに添付できるのはひとつのファイルのみです" + only-one-file-attached: "メッセージに添付できるファイルはひとつです" common/views/components/messaging-room.form.vue: input-message-here: "ここにメッセージを入力" send: "送信" attach-from-local: "PCからファイルを添付する" attach-from-drive: "ドライブからファイルを添付する" - only-one-file-attached: "メッセージに添付できるのはひとつのファイルのみです" + only-one-file-attached: "メッセージに添付できるファイルはひとつです" common/views/components/messaging-room.message.vue: is-read: "既読" @@ -750,11 +755,27 @@ common/views/components/user-list-editor.vue: remove-user: "このリストから削除" delete-are-you-sure: "リスト「$1」を削除しますか?" deleted: "削除しました" + add-user: "ユーザーを追加" + +common/views/components/user-group-editor.vue: + users: "メンバー" + rename: "グループ名を変更" + delete: "グループを削除" + remove-user: "このグループから削除" + delete-are-you-sure: "グループ「$1」を削除しますか?" + deleted: "削除しました" + add-user: "メンバーを追加" common/views/components/user-lists.vue: + user-lists: "リスト" create-list: "リストを作成" list-name: "リスト名" +common/views/components/user-groups.vue: + user-groups: "グループ" + create-group: "グループを作成" + group-name: "グループ名" + common/views/widgets/broadcast.vue: fetching: "確認中" no-broadcasts: "お知らせはありません" @@ -827,6 +848,11 @@ common/views/pages/follow.vue: follow-processing: "フォロー処理中" follow-request: "フォロー申請" +common/views/pages/follow-requests.vue: + received-follow-requests: "フォロー申請" + accept: "承認" + reject: "拒否" + desktop: banner-crop-title: "バナーとして表示する部分を選択" banner: "バナー" @@ -1139,6 +1165,7 @@ desktop/views/components/ui.header.vue: desktop/views/components/ui.header.account.vue: profile: "プロフィール" lists: "リスト" + groups: "グループ" follow-requests: "フォロー申請" admin: "管理" @@ -1154,14 +1181,6 @@ desktop/views/components/ui.header.post.vue: desktop/views/components/ui.header.search.vue: placeholder: "検索" -desktop/views/components/received-follow-requests-window.vue: - title: "フォロー申請" - accept: "承認" - reject: "拒否" - -desktop/views/components/user-lists-window.vue: - title: "リスト" - desktop/views/components/user-preview.vue: notes: "投稿" following: "フォロー" @@ -1749,11 +1768,6 @@ mobile/views/pages/widgets/activity.vue: mobile/views/pages/share.vue: share-with: "{name}で共有" -mobile/views/pages/received-follow-requests.vue: - title: "フォロー申請" - accept: "承認" - reject: "拒否" - mobile/views/pages/note.vue: title: "投稿" prev: "前の投稿" diff --git a/migration/1558103093633-UserGroup.ts b/migration/1558103093633-UserGroup.ts new file mode 100644 index 000000000..04783b8df --- /dev/null +++ b/migration/1558103093633-UserGroup.ts @@ -0,0 +1,41 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class UserGroup1558103093633 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "user_group" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, "isPrivate" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_3c29fba6fe013ec8724378ce7c9" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_20e30aa35180e317e133d75316" ON "user_group" ("createdAt") `); + await queryRunner.query(`CREATE INDEX "IDX_3d6b372788ab01be58853003c9" ON "user_group" ("userId") `); + await queryRunner.query(`CREATE TABLE "user_group_joining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_15f2425885253c5507e1599cfe7" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_f3a1b4bd0c7cabba958a0c0b23" ON "user_group_joining" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_67dc758bc0566985d1b3d39986" ON "user_group_joining" ("userGroupId") `); + await queryRunner.query(`ALTER TABLE "messaging_message" ADD "groupId" character varying(32)`); + await queryRunner.query(`ALTER TABLE "messaging_message" ADD "reads" character varying(32) array NOT NULL DEFAULT '{}'::varchar[]`); + await queryRunner.query(`ALTER TABLE "messaging_message" ALTER COLUMN "recipientId" DROP NOT NULL`); + await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."recipientId" IS 'The recipient user ID.'`); + await queryRunner.query(`CREATE INDEX "IDX_2c4be03b446884f9e9c502135b" ON "messaging_message" ("groupId") `); + await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_2c4be03b446884f9e9c502135be" FOREIGN KEY ("groupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_group" ADD CONSTRAINT "FK_3d6b372788ab01be58853003c93" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_group_joining" ADD CONSTRAINT "FK_f3a1b4bd0c7cabba958a0c0b231" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_group_joining" ADD CONSTRAINT "FK_67dc758bc0566985d1b3d399865" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_group_joining" DROP CONSTRAINT "FK_67dc758bc0566985d1b3d399865"`); + await queryRunner.query(`ALTER TABLE "user_group_joining" DROP CONSTRAINT "FK_f3a1b4bd0c7cabba958a0c0b231"`); + await queryRunner.query(`ALTER TABLE "user_group" DROP CONSTRAINT "FK_3d6b372788ab01be58853003c93"`); + await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_2c4be03b446884f9e9c502135be"`); + await queryRunner.query(`DROP INDEX "IDX_2c4be03b446884f9e9c502135b"`); + await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."recipientId" IS ''`); + await queryRunner.query(`ALTER TABLE "messaging_message" ALTER COLUMN "recipientId" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "reads"`); + await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "groupId"`); + await queryRunner.query(`DROP INDEX "IDX_67dc758bc0566985d1b3d39986"`); + await queryRunner.query(`DROP INDEX "IDX_f3a1b4bd0c7cabba958a0c0b23"`); + await queryRunner.query(`DROP TABLE "user_group_joining"`); + await queryRunner.query(`DROP INDEX "IDX_3d6b372788ab01be58853003c9"`); + await queryRunner.query(`DROP INDEX "IDX_20e30aa35180e317e133d75316"`); + await queryRunner.query(`DROP TABLE "user_group"`); + } + +} diff --git a/src/client/app/common/views/components/dialog.vue b/src/client/app/common/views/components/dialog.vue index f22e0174b..9f38031d6 100644 --- a/src/client/app/common/views/components/dialog.vue +++ b/src/client/app/common/views/components/dialog.vue @@ -18,6 +18,7 @@
    +
    {{ $t('@.enter-username') }}
    diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index f4d40f9b1..174fa36c0 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -44,6 +44,8 @@ import uiSwitch from './ui/switch.vue'; import uiRadio from './ui/radio.vue'; import uiSelect from './ui/select.vue'; import uiInfo from './ui/info.vue'; +import uiMargin from './ui/margin.vue'; +import uiHr from './ui/hr.vue'; import formButton from './ui/form/button.vue'; import formRadio from './ui/form/radio.vue'; @@ -91,5 +93,7 @@ Vue.component('ui-switch', uiSwitch); Vue.component('ui-radio', uiRadio); Vue.component('ui-select', uiSelect); Vue.component('ui-info', uiInfo); +Vue.component('ui-margin', uiMargin); +Vue.component('ui-hr', uiHr); Vue.component('form-button', formButton); Vue.component('form-radio', formRadio); diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue index ee6c312bc..1dfb0589e 100644 --- a/src/client/app/common/views/components/messaging-room.form.vue +++ b/src/client/app/common/views/components/messaging-room.form.vue @@ -33,7 +33,16 @@ import * as autosize from 'autosize'; export default Vue.extend({ i18n: i18n('common/views/components/messaging-room.form.vue'), - props: ['user'], + props: { + user: { + type: Object, + requird: false, + }, + group: { + type: Object, + requird: false, + }, + }, data() { return { text: null, @@ -43,7 +52,7 @@ export default Vue.extend({ }, computed: { draftId(): string { - return this.user.id; + return this.user ? 'user:' + this.user.id : 'group:' + this.group.id; }, canSend(): boolean { return (this.text != null && this.text != '') || this.file != null; @@ -159,7 +168,8 @@ export default Vue.extend({ send() { this.sending = true; this.$root.api('messaging/messages/create', { - userId: this.user.id, + userId: this.user ? this.user.id : undefined, + groupId: this.group ? this.group.id : undefined, text: this.text ? this.text : undefined, fileId: this.file ? this.file.id : undefined }).then(message => { diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue index 908533e0c..aff89c257 100644 --- a/src/client/app/common/views/components/messaging-room.message.vue +++ b/src/client/app/common/views/components/messaging-room.message.vue @@ -23,7 +23,12 @@
    - {{ $t('is-read') }} + +
    @@ -42,6 +47,9 @@ export default Vue.extend({ props: { message: { required: true + }, + isGroup: { + required: false } }, computed: { diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue index a8980e068..658dc93f6 100644 --- a/src/client/app/common/views/components/messaging-room.vue +++ b/src/client/app/common/views/components/messaging-room.vue @@ -4,14 +4,14 @@ @drop.prevent.stop="onDrop" >
    -

    {{ $t('@.loading') }}

    -

    {{ $t('empty') }}

    +

    {{ $t('@.loading') }}

    +

    {{ user ? $t('not-talked-user') : $t('not-talked-group') }}

    {{ $t('no-history') }}

    @@ -34,17 +34,30 @@ import i18n from '../../../i18n'; import XMessage from './messaging-room.message.vue'; import XForm from './messaging-room.form.vue'; import { url } from '../../../config'; -import { faArrowCircleDown } from '@fortawesome/free-solid-svg-icons'; -import { faFlag } from '@fortawesome/free-regular-svg-icons'; +import { faArrowCircleDown, faFlag } from '@fortawesome/free-solid-svg-icons'; export default Vue.extend({ i18n: i18n('common/views/components/messaging-room.vue'), + components: { XMessage, XForm }, - props: ['user', 'isNaked'], + props: { + user: { + type: Object, + requird: false, + }, + group: { + type: Object, + requird: false, + }, + isNaked: { + type: Boolean, + requird: false, + }, + }, data() { return { @@ -76,7 +89,10 @@ export default Vue.extend({ }, mounted() { - this.connection = this.$root.stream.connectToChannel('messaging', { otherparty: this.user.id }); + this.connection = this.$root.stream.connectToChannel('messaging', { + otherparty: this.user ? this.user.id : undefined, + group: this.group ? this.group.id : undefined, + }); this.connection.on('message', this.onMessage); this.connection.on('read', this.onRead); @@ -147,7 +163,8 @@ export default Vue.extend({ const max = this.existMoreMessages ? 20 : 10; this.$root.api('messaging/messages', { - userId: this.user.id, + userId: this.user ? this.user.id : undefined, + groupId: this.group ? this.group.id : undefined, limit: max + 1, untilId: this.existMoreMessages ? this.messages[0].id : undefined }).then(messages => { @@ -199,12 +216,21 @@ export default Vue.extend({ } }, - onRead(ids) { - if (!Array.isArray(ids)) ids = [ids]; - for (const id of ids) { - if (this.messages.some(x => x.id == id)) { - const exist = this.messages.map(x => x.id).indexOf(id); - this.messages[exist].isRead = true; + onRead(x) { + if (this.user) { + if (!Array.isArray(x)) x = [x]; + for (const id of x) { + if (this.messages.some(x => x.id == id)) { + const exist = this.messages.map(x => x.id).indexOf(id); + this.messages[exist].isRead = true; + } + } + } else if (this.group) { + for (const id of x.ids) { + if (this.messages.some(x => x.id == id)) { + const exist = this.messages.map(x => x.id).indexOf(id); + this.messages[exist].reads.push(x.userId); + } } } }, diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue index f884a599d..01d7a5a79 100644 --- a/src/client/app/common/views/components/messaging.vue +++ b/src/client/app/common/views/components/messaging.vue @@ -21,36 +21,62 @@
    - +
    +
    -

    {{ $t('no-history') }}

    + +

    {{ $t('no-history') }}

    {{ $t('@.loading') }}

    + + {{ $t('start-with-user') }} + {{ $t('start-with-group') }} +
    + + diff --git a/src/client/app/common/views/components/ui/margin.vue b/src/client/app/common/views/components/ui/margin.vue new file mode 100644 index 000000000..508116f07 --- /dev/null +++ b/src/client/app/common/views/components/ui/margin.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/src/client/app/common/views/components/user-lists.vue b/src/client/app/common/views/components/user-lists.vue deleted file mode 100644 index 699251b31..000000000 --- a/src/client/app/common/views/components/user-lists.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - - - diff --git a/src/client/app/common/views/components/user-menu.vue b/src/client/app/common/views/components/user-menu.vue index 7cbffa9f9..532dcf35c 100644 --- a/src/client/app/common/views/components/user-menu.vue +++ b/src/client/app/common/views/components/user-menu.vue @@ -27,7 +27,7 @@ export default Vue.extend({ text: this.$t('push-to-list'), action: this.pushList }] as any; - + if (this.$store.getters.isSignedIn && this.$store.state.i.id != this.user.id) { menu = menu.concat([null, { icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'], diff --git a/src/client/app/common/views/deck/deck.explore-column.vue b/src/client/app/common/views/deck/deck.column-template.vue similarity index 50% rename from src/client/app/common/views/deck/deck.explore-column.vue rename to src/client/app/common/views/deck/deck.column-template.vue index 53db677b3..09583de4b 100644 --- a/src/client/app/common/views/deck/deck.explore-column.vue +++ b/src/client/app/common/views/deck/deck.column-template.vue @@ -1,34 +1,45 @@ diff --git a/src/client/app/common/views/pages/explore.vue b/src/client/app/common/views/pages/explore.vue index d0e98035f..bf0d7ab57 100644 --- a/src/client/app/common/views/pages/explore.vue +++ b/src/client/app/common/views/pages/explore.vue @@ -116,6 +116,10 @@ export default Vue.extend({ }, created() { + this.$emit('init', { + title: this.$t('@.explore'), + icon: faHashtag + }); this.$root.api('hashtags/list', { sort: '+attachedLocalUsers', attachedToLocalUserOnly: true, diff --git a/src/client/app/mobile/views/pages/received-follow-requests.vue b/src/client/app/common/views/pages/follow-requests.vue similarity index 57% rename from src/client/app/mobile/views/pages/received-follow-requests.vue rename to src/client/app/common/views/pages/follow-requests.vue index abf0c3383..860efefd9 100644 --- a/src/client/app/mobile/views/pages/received-follow-requests.vue +++ b/src/client/app/common/views/pages/follow-requests.vue @@ -1,27 +1,30 @@ diff --git a/src/client/app/common/views/pages/user-groups.vue b/src/client/app/common/views/pages/user-groups.vue new file mode 100644 index 000000000..336772799 --- /dev/null +++ b/src/client/app/common/views/pages/user-groups.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/src/client/app/common/views/components/user-list-editor.vue b/src/client/app/common/views/pages/user-list-editor.vue similarity index 66% rename from src/client/app/common/views/components/user-list-editor.vue rename to src/client/app/common/views/pages/user-list-editor.vue index 86024c4da..6b2fd75f8 100644 --- a/src/client/app/common/views/components/user-list-editor.vue +++ b/src/client/app/common/views/pages/user-list-editor.vue @@ -1,18 +1,23 @@ + + diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index 464f7d3ce..c6479f477 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -22,6 +22,7 @@ import MkShare from '../common/views/pages/share.vue'; import MkFollow from '../common/views/pages/follow.vue'; import MkNotFound from '../common/views/pages/not-found.vue'; import MkSettings from './views/pages/settings.vue'; +import DeckColumn from '../common/views/deck/deck.column-template.vue'; import Ctx from './views/components/context-menu.vue'; import PostFormWindow from './views/components/post-form-window.vue'; @@ -138,9 +139,14 @@ init(async (launch, os) => { { path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) }, { path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) }, { path: '/featured', name: 'featured', component: () => import('../common/views/deck/deck.featured-column.vue').then(m => m.default) }, - { path: '/explore', name: 'explore', component: () => import('../common/views/deck/deck.explore-column.vue').then(m => m.default) }, - { path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/deck/deck.explore-column.vue').then(m => m.default) }, - { path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) } + { path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, + { path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, + { path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) }, + { path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, + { path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, + { path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) }, + { path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, + { path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) }, ]} : { path: '/', component: MkHome, children: [ { path: '', name: 'index', component: MkHomeTimeline }, @@ -157,11 +163,17 @@ init(async (launch, os) => { { path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, { path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) }, { path: '/i/pages', component: () => import('../common/views/pages/pages.vue').then(m => m.default) }, + { path: '/i/lists', component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }, + { path: '/i/lists/:listId', props: true, component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default) }, + { path: '/i/groups', component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }, + { path: '/i/groups/:groupId', props: true, component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default) }, + { path: '/i/follow-requests', component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }, ]}, { path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) }, { path: '/@:user/pages/:pageName/view-source', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, { path: '/i/pages/new', component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, { path: '/i/pages/edit/:pageId', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, + { path: '/i/messaging/group/:group', component: MkMessagingRoom }, { path: '/i/messaging/:user', component: MkMessagingRoom }, { path: '/i/drive', component: MkDrive }, { path: '/i/drive/folder/:folder', component: MkDrive }, diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue index 00cd423cd..6c1708b59 100644 --- a/src/client/app/desktop/views/components/messaging-room-window.vue +++ b/src/client/app/desktop/views/components/messaging-room-window.vue @@ -1,7 +1,7 @@ @@ -16,10 +16,14 @@ export default Vue.extend({ components: { XMessagingRoom: () => import('../../../common/views/components/messaging-room.vue').then(m => m.default) }, - props: ['user'], + props: ['user', 'group'], computed: { popout(): string { - return `${url}/i/messaging/${getAcct(this.user)}`; + if (this.user) { + return `${url}/i/messaging/${getAcct(this.user)}`; + } else if (this.group) { + return `${url}/i/messaging/group/${this.group.id}`; + } } } }); diff --git a/src/client/app/desktop/views/components/messaging-window.vue b/src/client/app/desktop/views/components/messaging-window.vue index 1572c4066..7cec9484d 100644 --- a/src/client/app/desktop/views/components/messaging-window.vue +++ b/src/client/app/desktop/views/components/messaging-window.vue @@ -1,7 +1,7 @@ @@ -20,6 +20,11 @@ export default Vue.extend({ this.$root.new(MkMessagingRoomWindow, { user: user }); + }, + navigateGroup(group) { + this.$root.new(MkMessagingRoomWindow, { + group: group + }); } } }); diff --git a/src/client/app/desktop/views/components/received-follow-requests-window.vue b/src/client/app/desktop/views/components/received-follow-requests-window.vue deleted file mode 100644 index f86b6b0d5..000000000 --- a/src/client/app/desktop/views/components/received-follow-requests-window.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - - - diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue index 9b87e0c29..c00c6b9c6 100644 --- a/src/client/app/desktop/views/components/ui.header.account.vue +++ b/src/client/app/desktop/views/components/ui.header.account.vue @@ -28,12 +28,19 @@ -
  • -

    +

  • + {{ $t('lists') }} -

    +
    +
  • +
  • + + + {{ $t('groups') }} + +
  • @@ -42,12 +49,12 @@
  • -
  • -

    +

  • + {{ $t('follow-requests') }}{{ $store.state.i.pendingReceivedFollowRequestsCount }} -

    +
    • @@ -96,12 +103,10 @@ diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue deleted file mode 100644 index afea01d4a..000000000 --- a/src/client/app/desktop/views/components/user-lists-window.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - - - diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue index 376b402d3..c725074b7 100644 --- a/src/client/app/desktop/views/pages/messaging-room.vue +++ b/src/client/app/desktop/views/pages/messaging-room.vue @@ -1,6 +1,6 @@ @@ -19,7 +19,8 @@ export default Vue.extend({ data() { return { fetching: true, - user: null + user: null, + group: null }; }, watch: { @@ -47,14 +48,25 @@ export default Vue.extend({ Progress.start(); this.fetching = true; - this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; + if (this.$route.params.user) { + this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { + this.user = user; + this.fetching = false; - document.title = this.$t('@.messaging') + ': ' + getUserName(this.user); + document.title = this.$t('@.messaging') + ': ' + getUserName(this.user); - Progress.done(); - }); + Progress.done(); + }); + } else { + this.$root.api('users/groups/show', { groupId: this.$route.params.group }).then(group => { + this.group = group; + this.fetching = false; + + document.title = this.$t('@.messaging') + ': ' + this.group.name; + + Progress.done(); + }); + } } } }); diff --git a/src/client/app/desktop/views/widgets/messaging.vue b/src/client/app/desktop/views/widgets/messaging.vue index 1e82ae3d3..e94e745c1 100644 --- a/src/client/app/desktop/views/widgets/messaging.vue +++ b/src/client/app/desktop/views/widgets/messaging.vue @@ -4,7 +4,7 @@ - +
    @@ -31,6 +31,11 @@ export default define({ user: user }); }, + navigateGroup(group) { + this.$root.new(MkMessagingRoomWindow, { + group: group + }); + }, add() { this.$root.new(MkMessagingWindow); }, diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts index 4a79d8877..360da0149 100644 --- a/src/client/app/mobile/script.ts +++ b/src/client/app/mobile/script.ts @@ -18,17 +18,16 @@ import MkDrive from './views/pages/drive.vue'; import MkWidgets from './views/pages/widgets.vue'; import MkMessaging from './views/pages/messaging.vue'; import MkMessagingRoom from './views/pages/messaging-room.vue'; -import MkReceivedFollowRequests from './views/pages/received-follow-requests.vue'; import MkNote from './views/pages/note.vue'; import MkSearch from './views/pages/search.vue'; import MkFavorites from './views/pages/favorites.vue'; -import MkUserLists from './views/pages/user-lists.vue'; -import MkUserList from './views/pages/user-list.vue'; +import UI from './views/pages/ui.vue'; import MkReversi from './views/pages/games/reversi.vue'; import MkTag from './views/pages/tag.vue'; import MkShare from '../common/views/pages/share.vue'; import MkFollow from '../common/views/pages/follow.vue'; import MkNotFound from '../common/views/pages/not-found.vue'; +import DeckColumn from '../common/views/deck/deck.column-template.vue'; import PostForm from './views/components/post-form-dialog.vue'; import FileChooser from './views/components/drive-file-chooser.vue'; @@ -125,9 +124,14 @@ init((launch, os) => { { path: '/search', component: () => import('../common/views/deck/deck.search-column.vue').then(m => m.default) }, { path: '/tags/:tag', name: 'tag', component: () => import('../common/views/deck/deck.hashtag-column.vue').then(m => m.default) }, { path: '/featured', name: 'featured', component: () => import('../common/views/deck/deck.featured-column.vue').then(m => m.default) }, - { path: '/explore', name: 'explore', component: () => import('../common/views/deck/deck.explore-column.vue').then(m => m.default) }, - { path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/deck/deck.explore-column.vue').then(m => m.default) }, - { path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) } + { path: '/explore', name: 'explore', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, + { path: '/explore/tags/:tag', name: 'explore-tag', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, + { path: '/i/favorites', component: () => import('../common/views/deck/deck.favorites-column.vue').then(m => m.default) }, + { path: '/i/pages', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, + { path: '/i/lists', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, + { path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) }, + { path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, + { path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) }, ]}] : [ { path: '/', name: 'index', component: MkIndex }, @@ -135,12 +139,15 @@ init((launch, os) => { { path: '/signup', name: 'signup', component: MkSignup }, { path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) }, { path: '/i/favorites', name: 'favorites', component: MkFavorites }, - { path: '/i/pages', name: 'pages', component: () => import('./views/pages/pages.vue').then(m => m.default) }, - { path: '/i/lists', name: 'user-lists', component: MkUserLists }, - { path: '/i/lists/:list', name: 'user-list', component: MkUserList }, - { path: '/i/received-follow-requests', name: 'received-follow-requests', component: MkReceivedFollowRequests }, + { path: '/i/pages', name: 'pages', component: UI, props: route => ({ component: () => import('../common/views/pages/pages.vue').then(m => m.default) }) }, + { path: '/i/lists', name: 'user-lists', component: UI, props: route => ({ component: () => import('../common/views/pages/user-lists.vue').then(m => m.default) }) }, + { path: '/i/lists/:list', component: UI, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.list }) }, + { path: '/i/groups', name: 'user-groups', component: UI, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, + { path: '/i/groups/:group', component: UI, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.group }) }, + { path: '/i/follow-requests', name: 'follow-requests', component: UI, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) }, { path: '/i/widgets', name: 'widgets', component: MkWidgets }, { path: '/i/messaging', name: 'messaging', component: MkMessaging }, + { path: '/i/messaging/group/:group', component: MkMessagingRoom }, { path: '/i/messaging/:user', component: MkMessagingRoom }, { path: '/i/drive', name: 'drive', component: MkDrive }, { path: '/i/drive/folder/:folder', component: MkDrive }, @@ -151,8 +158,8 @@ init((launch, os) => { { path: '/search', component: MkSearch }, { path: '/tags/:tag', component: MkTag }, { path: '/featured', name: 'featured', component: () => import('./views/pages/featured.vue').then(m => m.default) }, - { path: '/explore', name: 'explore', component: () => import('./views/pages/explore.vue').then(m => m.default) }, - { path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('./views/pages/explore.vue').then(m => m.default) }, + { path: '/explore', name: 'explore', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default) }) }, + { path: '/explore/tags/:tag', name: 'explore-tag', component: UI, props: route => ({ component: () => import('../common/views/pages/explore.vue').then(m => m.default), tag: route.params.tag }) }, { path: '/share', component: MkShare }, { path: '/games/reversi/:game?', name: 'reversi', component: MkReversi }, { path: '/@:user', name: 'user', component: () => import('./views/pages/user/index.vue').then(m => m.default), children: [ diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue index da9bb518e..29c744d89 100644 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ b/src/client/app/mobile/views/components/ui.nav.vue @@ -19,7 +19,7 @@
  • {{ $t('timeline') }}
  • {{ $t('notifications') }}

  • {{ $t('@.messaging') }}
  • -
  • {{ $t('follow-requests') }}
  • +
  • {{ $t('follow-requests') }}
  • {{ $t('@.featured-notes') }}
  • {{ $t('@.explore') }}
  • {{ $t('game') }}
  • diff --git a/src/client/app/mobile/views/pages/explore.vue b/src/client/app/mobile/views/pages/explore.vue deleted file mode 100644 index 111721bc8..000000000 --- a/src/client/app/mobile/views/pages/explore.vue +++ /dev/null @@ -1,28 +0,0 @@ - - - diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue index aa00d4869..787284712 100644 --- a/src/client/app/mobile/views/pages/messaging-room.vue +++ b/src/client/app/mobile/views/pages/messaging-room.vue @@ -2,9 +2,10 @@ - + @@ -22,6 +23,7 @@ export default Vue.extend({ return { fetching: true, user: null, + group: null, unwatchDarkmode: null }; }, @@ -48,12 +50,21 @@ export default Vue.extend({ methods: { fetch() { this.fetching = true; - this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { - this.user = user; - this.fetching = false; + if (this.$route.params.user) { + this.$root.api('users/show', parseAcct(this.$route.params.user)).then(user => { + this.user = user; + this.fetching = false; - document.title = `${this.$t('@.messaging')}: ${Vue.filter('userName')(this.user)} | ${this.$root.instanceName}`; - }); + document.title = `${this.$t('@.messaging')}: ${Vue.filter('userName')(this.user)} | ${this.$root.instanceName}`; + }); + } else { + this.$root.api('users/groups/show', { groupId: this.$route.params.group }).then(group => { + this.group = group; + this.fetching = false; + + document.title = this.$t('@.messaging') + ': ' + this.group.name; + }); + } } } }); diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue index 5ce2f14bb..ff66ae06e 100644 --- a/src/client/app/mobile/views/pages/messaging.vue +++ b/src/client/app/mobile/views/pages/messaging.vue @@ -1,7 +1,7 @@ @@ -21,6 +21,9 @@ export default Vue.extend({ methods: { navigate(user) { (this as any).$router.push(`/i/messaging/${getAcct(user)}`); + }, + navigateGroup(group) { + (this as any).$router.push(`/i/messaging/group/${group.id}`); } } }); diff --git a/src/client/app/mobile/views/pages/pages.vue b/src/client/app/mobile/views/pages/pages.vue deleted file mode 100644 index 2fd134fcd..000000000 --- a/src/client/app/mobile/views/pages/pages.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - - diff --git a/src/client/app/mobile/views/pages/ui.vue b/src/client/app/mobile/views/pages/ui.vue new file mode 100644 index 000000000..397ba5df0 --- /dev/null +++ b/src/client/app/mobile/views/pages/ui.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/client/app/mobile/views/pages/user-list.vue b/src/client/app/mobile/views/pages/user-list.vue deleted file mode 100644 index 68fd0358c..000000000 --- a/src/client/app/mobile/views/pages/user-list.vue +++ /dev/null @@ -1,48 +0,0 @@ - - - diff --git a/src/client/app/mobile/views/pages/user-lists.vue b/src/client/app/mobile/views/pages/user-lists.vue deleted file mode 100644 index a3e9bd78b..000000000 --- a/src/client/app/mobile/views/pages/user-lists.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/src/db/postgre.ts b/src/db/postgre.ts index f488af03c..40b9ce151 100644 --- a/src/db/postgre.ts +++ b/src/db/postgre.ts @@ -24,6 +24,8 @@ import { SwSubscription } from '../models/entities/sw-subscription'; import { Blocking } from '../models/entities/blocking'; import { UserList } from '../models/entities/user-list'; import { UserListJoining } from '../models/entities/user-list-joining'; +import { UserGroup } from '../models/entities/user-group'; +import { UserGroupJoining } from '../models/entities/user-group-joining'; import { Hashtag } from '../models/entities/hashtag'; import { NoteFavorite } from '../models/entities/note-favorite'; import { AbuseUserReport } from '../models/entities/abuse-user-report'; @@ -106,6 +108,8 @@ export function initDb(justBorrow = false, sync = false, log = false) { UserPublickey, UserList, UserListJoining, + UserGroup, + UserGroupJoining, UserNotePining, Following, FollowRequest, diff --git a/src/models/entities/messaging-message.ts b/src/models/entities/messaging-message.ts index d3c3eab3a..c18897a37 100644 --- a/src/models/entities/messaging-message.ts +++ b/src/models/entities/messaging-message.ts @@ -2,6 +2,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ import { User } from './user'; import { DriveFile } from './drive-file'; import { id } from '../id'; +import { UserGroup } from './user-group'; @Entity() export class MessagingMessage { @@ -29,10 +30,10 @@ export class MessagingMessage { @Index() @Column({ - ...id(), + ...id(), nullable: true, comment: 'The recipient user ID.' }) - public recipientId: User['id']; + public recipientId: User['id'] | null; @ManyToOne(type => User, { onDelete: 'CASCADE' @@ -40,6 +41,19 @@ export class MessagingMessage { @JoinColumn() public recipient: User | null; + @Index() + @Column({ + ...id(), nullable: true, + comment: 'The recipient group ID.' + }) + public groupId: UserGroup['id'] | null; + + @ManyToOne(type => UserGroup, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public group: UserGroup | null; + @Column('varchar', { length: 4096, nullable: true }) @@ -50,6 +64,12 @@ export class MessagingMessage { }) public isRead: boolean; + @Column({ + ...id(), + array: true, default: '{}' + }) + public reads: User['id'][]; + @Column({ ...id(), nullable: true, diff --git a/src/models/entities/user-group-joining.ts b/src/models/entities/user-group-joining.ts new file mode 100644 index 000000000..17b534f42 --- /dev/null +++ b/src/models/entities/user-group-joining.ts @@ -0,0 +1,41 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { UserGroup } from './user-group'; +import { id } from '../id'; + +@Entity() +export class UserGroupJoining { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserGroupJoining.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The user ID.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The group ID.' + }) + public userGroupId: UserGroup['id']; + + @ManyToOne(type => UserGroup, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public userGroup: UserGroup | null; +} diff --git a/src/models/entities/user-group.ts b/src/models/entities/user-group.ts new file mode 100644 index 000000000..f4bac0322 --- /dev/null +++ b/src/models/entities/user-group.ts @@ -0,0 +1,46 @@ +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class UserGroup { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the UserGroup.' + }) + public createdAt: Date; + + @Column('varchar', { + length: 256, + }) + public name: string; + + @Index() + @Column({ + ...id(), + comment: 'The ID of owner.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column('boolean', { + default: false, + }) + public isPrivate: boolean; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/src/models/index.ts b/src/models/index.ts index a63bb2c2b..c05d7febe 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -6,7 +6,6 @@ import { PollVote } from './entities/poll-vote'; import { Meta } from './entities/meta'; import { SwSubscription } from './entities/sw-subscription'; import { NoteWatching } from './entities/note-watching'; -import { UserListJoining } from './entities/user-list-joining'; import { NoteUnread } from './entities/note-unread'; import { RegistrationTicket } from './entities/registration-tickets'; import { UserRepository } from './repositories/user'; @@ -20,6 +19,9 @@ import { SigninRepository } from './repositories/signin'; import { MessagingMessageRepository } from './repositories/messaging-message'; import { ReversiGameRepository } from './repositories/games/reversi/game'; import { UserListRepository } from './repositories/user-list'; +import { UserListJoining } from './entities/user-list-joining'; +import { UserGroupRepository } from './repositories/user-group'; +import { UserGroupJoining } from './entities/user-group-joining'; import { FollowRequestRepository } from './repositories/follow-request'; import { MutingRepository } from './repositories/muting'; import { BlockingRepository } from './repositories/blocking'; @@ -52,6 +54,8 @@ export const UserKeypairs = getRepository(UserKeypair); export const UserPublickeys = getRepository(UserPublickey); export const UserLists = getCustomRepository(UserListRepository); export const UserListJoinings = getRepository(UserListJoining); +export const UserGroups = getCustomRepository(UserGroupRepository); +export const UserGroupJoinings = getRepository(UserGroupJoining); export const UserNotePinings = getRepository(UserNotePining); export const Followings = getCustomRepository(FollowingRepository); export const FollowRequests = getCustomRepository(FollowRequestRepository); diff --git a/src/models/repositories/messaging-message.ts b/src/models/repositories/messaging-message.ts index 33f95bbd5..a64ed0732 100644 --- a/src/models/repositories/messaging-message.ts +++ b/src/models/repositories/messaging-message.ts @@ -1,6 +1,6 @@ import { EntityRepository, Repository } from 'typeorm'; import { MessagingMessage } from '../entities/messaging-message'; -import { Users, DriveFiles } from '..'; +import { Users, DriveFiles, UserGroups } from '..'; import { ensure } from '../../prelude/ensure'; import { types, bool, SchemaType } from '../../misc/schema'; @@ -16,11 +16,13 @@ export class MessagingMessageRepository extends Repository { src: MessagingMessage['id'] | MessagingMessage, me?: any, options?: { - populateRecipient: boolean + populateRecipient?: boolean, + populateGroup?: boolean, } ): Promise { const opts = options || { - populateRecipient: true + populateRecipient: true, + populateGroup: true, }; const message = typeof src === 'object' ? src : await this.findOne(src).then(ensure); @@ -32,10 +34,13 @@ export class MessagingMessageRepository extends Repository { userId: message.userId, user: await Users.pack(message.user || message.userId, me), recipientId: message.recipientId, - recipient: opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : undefined, + recipient: message.recipientId && opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : undefined, + groupId: message.recipientId, + group: message.groupId && opts.populateGroup ? await UserGroups.pack(message.group || message.groupId) : undefined, fileId: message.fileId, file: message.fileId ? await DriveFiles.pack(message.fileId) : null, - isRead: message.isRead + isRead: message.isRead, + reads: message.reads, }; } } @@ -83,17 +88,36 @@ export const packedMessagingMessageSchema = { }, recipientId: { type: types.string, - optional: bool.false, nullable: bool.false, + optional: bool.false, nullable: bool.true, format: 'id', }, recipient: { type: types.object, - optional: bool.true, nullable: bool.false, + optional: bool.true, nullable: bool.true, ref: 'User' }, + groupId: { + type: types.string, + optional: bool.false, nullable: bool.true, + format: 'id', + }, + group: { + type: types.object, + optional: bool.true, nullable: bool.true, + ref: 'UserGroup' + }, isRead: { type: types.boolean, optional: bool.true, nullable: bool.false, }, + reads: { + type: types.array, + optional: bool.true, nullable: bool.false, + items: { + type: types.string, + optional: bool.false, nullable: bool.false, + format: 'id' + } + }, }, }; diff --git a/src/models/repositories/user-group.ts b/src/models/repositories/user-group.ts new file mode 100644 index 000000000..8bb1ae833 --- /dev/null +++ b/src/models/repositories/user-group.ts @@ -0,0 +1,61 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { UserGroup } from '../entities/user-group'; +import { ensure } from '../../prelude/ensure'; +import { UserGroupJoinings } from '..'; +import { bool, types, SchemaType } from '../../misc/schema'; + +export type PackedUserGroup = SchemaType; + +@EntityRepository(UserGroup) +export class UserGroupRepository extends Repository { + public async pack( + src: UserGroup['id'] | UserGroup, + ): Promise { + const userGroup = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + const users = await UserGroupJoinings.find({ + userGroupId: userGroup.id + }); + + return { + id: userGroup.id, + createdAt: userGroup.createdAt.toISOString(), + name: userGroup.name, + userIds: users.map(x => x.userId) + }; + } +} + +export const packedUserGroupSchema = { + type: types.object, + optional: bool.false, nullable: bool.false, + properties: { + id: { + type: types.string, + optional: bool.false, nullable: bool.false, + format: 'id', + description: 'The unique identifier for this UserGroup.', + example: 'xxxxxxxxxx', + }, + createdAt: { + type: types.string, + optional: bool.false, nullable: bool.false, + format: 'date-time', + description: 'The date that the UserGroup was created.' + }, + name: { + type: types.string, + optional: bool.false, nullable: bool.false, + description: 'The name of the UserGroup.' + }, + userIds: { + type: types.array, + nullable: bool.false, optional: bool.true, + items: { + type: types.string, + nullable: bool.false, optional: bool.false, + format: 'id', + } + }, + }, +}; diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts index 330220fb7..f81fa6bc7 100644 --- a/src/models/repositories/user.ts +++ b/src/models/repositories/user.ts @@ -1,6 +1,6 @@ import { EntityRepository, Repository, In } from 'typeorm'; import { User, ILocalUser, IRemoteUser } from '../entities/user'; -import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles } from '..'; +import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserGroupJoinings } from '..'; import { ensure } from '../../prelude/ensure'; import config from '../../config'; import { SchemaType, bool, types } from '../../misc/schema'; @@ -54,6 +54,31 @@ export class UserRepository extends Repository { }; } + public async getHasUnreadMessagingMessage(userId: User['id']): Promise { + const joinings = await UserGroupJoinings.find({ userId: userId }); + + const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message') + .where(`message.groupId = :groupId`, { groupId: j.userGroupId }) + .andWhere('message.userId != :userId', { userId: userId }) + .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) + .andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない + .getOne().then(x => x != null))); + + const [withUser, withGroups] = await Promise.all([ + // TODO: ミュートを考慮 + MessagingMessages.count({ + where: { + recipientId: userId, + isRead: false + }, + take: 1 + }).then(count => count > 0), + groupQs + ]); + + return withUser || withGroups.some(x => x); + } + public async pack( src: User['id'] | User, me?: User['id'] | User | null | undefined, @@ -151,13 +176,7 @@ export class UserRepository extends Repository { autoWatch: profile!.autoWatch, alwaysMarkNsfw: profile!.alwaysMarkNsfw, carefulBot: profile!.carefulBot, - hasUnreadMessagingMessage: MessagingMessages.count({ - where: { - recipientId: user.id, - isRead: false - }, - take: 1 - }).then(count => count > 0), + hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id), hasUnreadNotification: Notifications.count({ where: { notifieeId: user.id, diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts index 2cb5a1f87..544d89019 100644 --- a/src/server/api/common/read-messaging-message.ts +++ b/src/server/api/common/read-messaging-message.ts @@ -1,21 +1,33 @@ -import { publishMainStream } from '../../../services/stream'; +import { publishMainStream, publishGroupMessagingStream } from '../../../services/stream'; import { publishMessagingStream } from '../../../services/stream'; import { publishMessagingIndexStream } from '../../../services/stream'; import { User } from '../../../models/entities/user'; import { MessagingMessage } from '../../../models/entities/messaging-message'; -import { MessagingMessages } from '../../../models'; +import { MessagingMessages, UserGroupJoinings, Users } from '../../../models'; import { In } from 'typeorm'; +import { IdentifiableError } from '../../../misc/identifiable-error'; +import { UserGroup } from '../../../models/entities/user-group'; /** * Mark messages as read */ -export default async ( +export async function readUserMessagingMessage( userId: User['id'], otherpartyId: User['id'], messageIds: MessagingMessage['id'][] -) => { +) { if (messageIds.length === 0) return; + const messages = await MessagingMessages.find({ + id: In(messageIds) + }); + + for (const message of messages) { + if (message.recipientId !== userId) { + throw new IdentifiableError('e140a4bf-49ce-4fb6-b67c-b78dadf6b52f', 'Access denied (user).'); + } + } + // Update documents await MessagingMessages.update({ id: In(messageIds), @@ -30,14 +42,62 @@ export default async ( publishMessagingStream(otherpartyId, userId, 'read', messageIds); publishMessagingIndexStream(userId, 'read', messageIds); - // Calc count of my unread messages - const count = await MessagingMessages.count({ - recipientId: userId, - isRead: false - }); - - if (count == 0) { + if (!Users.getHasUnreadMessagingMessage(userId)) { // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 publishMainStream(userId, 'readAllMessagingMessages'); } -}; +} + +/** + * Mark messages as read + */ +export async function readGroupMessagingMessage( + userId: User['id'], + groupId: UserGroup['id'], + messageIds: MessagingMessage['id'][] +) { + if (messageIds.length === 0) return; + + // check joined + const joining = await UserGroupJoinings.findOne({ + userId: userId, + userGroupId: groupId + }); + + if (joining == null) { + throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).'); + } + + const messages = await MessagingMessages.find({ + id: In(messageIds) + }); + + const reads = []; + + for (const message of messages) { + if (message.userId === userId) continue; + if (message.reads.includes(userId)) continue; + + // Update document + await MessagingMessages.createQueryBuilder().update() + .set({ + reads: (() => `array_append("reads", '${joining.userId}')`) as any + }) + .where('id = :id', { id: message.id }) + .execute(); + + reads.push(message.id); + } + + // Publish event + publishGroupMessagingStream(groupId, 'read', { + ids: reads, + userId: userId + }); + publishMessagingIndexStream(userId, 'read', reads); + + if (!Users.getHasUnreadMessagingMessage(userId)) { + // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 + publishMainStream(userId, 'readAllMessagingMessages'); + } +} diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts index 27e38bbde..833ec37e4 100644 --- a/src/server/api/endpoints/messaging/history.ts +++ b/src/server/api/endpoints/messaging/history.ts @@ -1,13 +1,13 @@ import $ from 'cafy'; import define from '../../define'; import { MessagingMessage } from '../../../../models/entities/messaging-message'; -import { MessagingMessages, Mutings } from '../../../../models'; +import { MessagingMessages, Mutings, UserGroupJoinings } from '../../../../models'; import { Brackets } from 'typeorm'; import { types, bool } from '../../../../misc/schema'; export const meta = { desc: { - 'ja-JP': 'Messagingの履歴を取得します。', + 'ja-JP': 'トークの履歴を取得します。', 'en-US': 'Show messaging history.' }, @@ -21,6 +21,11 @@ export const meta = { limit: { validator: $.optional.num.range(1, 100), default: 10 + }, + + group: { + validator: $.optional.bool, + default: false } }, @@ -40,26 +45,46 @@ export default define(meta, async (ps, user) => { muterId: user.id, }); + const groups = ps.group ? await UserGroupJoinings.find({ + userId: user.id, + }).then(xs => xs.map(x => x.userGroupId)) : []; + + if (ps.group && groups.length === 0) { + return []; + } + const history: MessagingMessage[] = []; for (let i = 0; i < ps.limit!; i++) { - const found = history.map(m => (m.userId === user.id) ? m.recipientId : m.userId); + const found = ps.group + ? history.map(m => m.groupId!) + : history.map(m => (m.userId === user.id) ? m.recipientId! : m.userId!); const query = MessagingMessages.createQueryBuilder('message') - .where(new Brackets(qb => { qb - .where(`message.userId = :userId`, { userId: user.id }) - .orWhere(`message.recipientId = :userId`, { userId: user.id }); - })) .orderBy('message.createdAt', 'DESC'); - if (found.length > 0) { - query.andWhere(`message.userId NOT IN (:...found)`, { found: found }); - query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found }); - } + if (ps.group) { + query.where(`message.groupId IN (:...groups)`, { groups: groups }); - if (mute.length > 0) { - query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); - query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); + if (found.length > 0) { + query.andWhere(`message.groupId NOT IN (:...found)`, { found: found }); + } + } else { + query.where(new Brackets(qb => { qb + .where(`message.userId = :userId`, { userId: user.id }) + .orWhere(`message.recipientId = :userId`, { userId: user.id }); + })); + query.andWhere(`message.groupId IS NULL`); + + if (found.length > 0) { + query.andWhere(`message.userId NOT IN (:...found)`, { found: found }); + query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found }); + } + + if (mute.length > 0) { + query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); + query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); + } } const message = await query.getOne(); diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts index 0d5295bff..c1e79cd13 100644 --- a/src/server/api/endpoints/messaging/messages.ts +++ b/src/server/api/endpoints/messaging/messages.ts @@ -1,16 +1,17 @@ import $ from 'cafy'; import { ID } from '../../../../misc/cafy-id'; -import read from '../../common/read-messaging-message'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; -import { MessagingMessages } from '../../../../models'; +import { MessagingMessages, UserGroups, UserGroupJoinings } from '../../../../models'; import { makePaginationQuery } from '../../common/make-pagination-query'; import { types, bool } from '../../../../misc/schema'; +import { Brackets } from 'typeorm'; +import { readUserMessagingMessage, readGroupMessagingMessage } from '../../common/read-messaging-message'; export const meta = { desc: { - 'ja-JP': '指定したユーザーとのMessagingのメッセージ一覧を取得します。', + 'ja-JP': 'トークメッセージ一覧を取得します。', 'en-US': 'Get messages of messaging.' }, @@ -22,13 +23,21 @@ export const meta = { params: { userId: { - validator: $.type(ID), + validator: $.optional.type(ID), desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' } }, + groupId: { + validator: $.optional.type(ID), + desc: { + 'ja-JP': '対象のグループのID', + 'en-US': 'Target group ID' + } + }, + limit: { validator: $.optional.num.range(1, 100), default: 10 @@ -64,27 +73,85 @@ export const meta = { code: 'NO_SUCH_USER', id: '11795c64-40ea-4198-b06e-3c873ed9039d' }, + + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f' + }, + + groupAccessDenied: { + message: 'You can not read messages of groups that you have not joined.', + code: 'GROUP_ACCESS_DENIED', + id: 'a053a8dd-a491-4718-8f87-50775aad9284' + }, } }; export default define(meta, async (ps, user) => { - // Fetch recipient - const recipient = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); + if (ps.userId != null) { + // Fetch recipient (user) + const recipient = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); - const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) - .andWhere(`(message.userId = :meId AND message.recipientId = :recipientId) OR (message.userId = :recipientId AND message.recipientId = :meId)`, { meId: user.id, recipientId: recipient.id }); + const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) + .andWhere(new Brackets(qb => { qb + .where(new Brackets(qb => { qb + .where('message.userId = :meId') + .andWhere('message.recipientId = :recipientId'); + })) + .orWhere(new Brackets(qb => { qb + .where('message.userId = :recipientId') + .andWhere('message.recipientId = :meId'); + })); + })) + .setParameter('meId', user.id) + .setParameter('recipientId', recipient.id); - const messages = await query.getMany(); + const messages = await query.take(ps.limit!).getMany(); - // Mark all as read - if (ps.markAsRead) { - read(user.id, recipient.id, messages.map(x => x.id)); + // Mark all as read + if (ps.markAsRead) { + readUserMessagingMessage(user.id, recipient.id, messages.map(x => x.id)); + } + + return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { + populateRecipient: false + }))); + } else if (ps.groupId != null) { + // Fetch recipient (group) + const recipientGroup = await UserGroups.findOne(ps.groupId); + + if (recipientGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // check joined + const joining = await UserGroupJoinings.findOne({ + userId: user.id, + userGroupId: recipientGroup.id + }); + + if (joining == null) { + throw new ApiError(meta.errors.groupAccessDenied); + } + + const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) + .andWhere(`message.groupId = :groupId`, { groupId: recipientGroup.id }); + + const messages = await query.take(ps.limit!).getMany(); + + // Mark all as read + if (ps.markAsRead) { + readGroupMessagingMessage(user.id, recipientGroup.id, messages.map(x => x.id)); + } + + return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { + populateGroup: false + }))); + } else { + throw new Error(); } - - return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { - populateRecipient: false - }))); }); diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts index 388852b9c..f5d7cf2b3 100644 --- a/src/server/api/endpoints/messaging/messages/create.ts +++ b/src/server/api/endpoints/messaging/messages/create.ts @@ -1,19 +1,22 @@ import $ from 'cafy'; import { ID } from '../../../../../misc/cafy-id'; -import { publishMainStream } from '../../../../../services/stream'; +import { publishMainStream, publishGroupMessagingStream } from '../../../../../services/stream'; import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../services/stream'; import pushSw from '../../../../../services/push-notification'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getUser } from '../../../common/getters'; -import { MessagingMessages, DriveFiles, Mutings } from '../../../../../models'; +import { MessagingMessages, DriveFiles, Mutings, UserGroups, UserGroupJoinings } from '../../../../../models'; import { MessagingMessage } from '../../../../../models/entities/messaging-message'; import { genId } from '../../../../../misc/gen-id'; import { types, bool } from '../../../../../misc/schema'; +import { User } from '../../../../../models/entities/user'; +import { UserGroup } from '../../../../../models/entities/user-group'; +import { Not } from 'typeorm'; export const meta = { desc: { - 'ja-JP': '指定したユーザーへMessagingのメッセージを送信します。', + 'ja-JP': 'トークメッセージを送信します。', 'en-US': 'Create a message of messaging.' }, @@ -25,13 +28,21 @@ export const meta = { params: { userId: { - validator: $.type(ID), + validator: $.optional.type(ID), desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' } }, + groupId: { + validator: $.optional.type(ID), + desc: { + 'ja-JP': '対象のグループのID', + 'en-US': 'Target group ID' + } + }, + text: { validator: $.optional.str.pipe(MessagingMessages.isValidText) }, @@ -60,6 +71,18 @@ export const meta = { id: '11795c64-40ea-4198-b06e-3c873ed9039d' }, + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: 'c94e2a5d-06aa-4914-8fa6-6a42e73d6537' + }, + + groupAccessDenied: { + message: 'You can not send messages to groups that you have not joined.', + code: 'GROUP_ACCESS_DENIED', + id: 'd96b3cca-5ad1-438b-ad8b-02f931308fbd' + }, + noSuchFile: { message: 'No such file.', code: 'NO_SUCH_FILE', @@ -75,16 +98,38 @@ export const meta = { }; export default define(meta, async (ps, user) => { - // Myself - if (ps.userId === user.id) { - throw new ApiError(meta.errors.recipientIsYourself); - } + let recipientUser: User | undefined; + let recipientGroup: UserGroup | undefined; - // Fetch recipient - const recipient = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw e; - }); + if (ps.userId != null) { + // Myself + if (ps.userId === user.id) { + throw new ApiError(meta.errors.recipientIsYourself); + } + + // Fetch recipient (user) + recipientUser = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); + } else if (ps.groupId != null) { + // Fetch recipient (group) + recipientGroup = await UserGroups.findOne(ps.groupId); + + if (recipientGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // check joined + const joining = await UserGroupJoinings.findOne({ + userId: user.id, + userGroupId: recipientGroup.id + }); + + if (joining == null) { + throw new ApiError(meta.errors.groupAccessDenied); + } + } let file = null; if (ps.fileId != null) { @@ -107,32 +152,49 @@ export default define(meta, async (ps, user) => { id: genId(), createdAt: new Date(), fileId: file ? file.id : null, - recipientId: recipient.id, + recipientId: recipientUser ? recipientUser.id : null, + groupId: recipientGroup ? recipientGroup.id : null, text: ps.text ? ps.text.trim() : null, userId: user.id, - isRead: false + isRead: false, + reads: [] as any[] } as MessagingMessage); const messageObj = await MessagingMessages.pack(message); - // 自分のストリーム - publishMessagingStream(message.userId, message.recipientId, 'message', messageObj); - publishMessagingIndexStream(message.userId, 'message', messageObj); - publishMainStream(message.userId, 'messagingMessage', messageObj); + if (recipientUser) { + // 自分のストリーム + publishMessagingStream(message.userId, recipientUser.id, 'message', messageObj); + publishMessagingIndexStream(message.userId, 'message', messageObj); + publishMainStream(message.userId, 'messagingMessage', messageObj); - // 相手のストリーム - publishMessagingStream(message.recipientId, message.userId, 'message', messageObj); - publishMessagingIndexStream(message.recipientId, 'message', messageObj); - publishMainStream(message.recipientId, 'messagingMessage', messageObj); + // 相手のストリーム + publishMessagingStream(recipientUser.id, message.userId, 'message', messageObj); + publishMessagingIndexStream(recipientUser.id, 'message', messageObj); + publishMainStream(recipientUser.id, 'messagingMessage', messageObj); + } else if (recipientGroup) { + // グループのストリーム + publishGroupMessagingStream(recipientGroup.id, 'message', messageObj); + + // メンバーのストリーム + const joinings = await UserGroupJoinings.find({ userGroupId: recipientGroup.id }); + for (const joining of joinings) { + publishMessagingIndexStream(joining.userId, 'message', messageObj); + publishMainStream(joining.userId, 'messagingMessage', messageObj); + } + } // 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する setTimeout(async () => { - const freshMessage = await MessagingMessages.findOne({ id: message.id }); + const freshMessage = await MessagingMessages.findOne(message.id); if (freshMessage == null) return; // メッセージが削除されている場合もある - if (!freshMessage.isRead) { + + if (recipientUser) { + if (freshMessage.isRead) return; // 既読 + //#region ただしミュートされているなら発行しない const mute = await Mutings.find({ - muterId: recipient.id, + muterId: recipientUser.id, }); const mutedUserIds = mute.map(m => m.muteeId.toString()); if (mutedUserIds.indexOf(user.id) != -1) { @@ -140,8 +202,15 @@ export default define(meta, async (ps, user) => { } //#endregion - publishMainStream(message.recipientId, 'unreadMessagingMessage', messageObj); - pushSw(message.recipientId, 'unreadMessagingMessage', messageObj); + publishMainStream(recipientUser.id, 'unreadMessagingMessage', messageObj); + pushSw(recipientUser.id, 'unreadMessagingMessage', messageObj); + } else if (recipientGroup) { + const joinings = await UserGroupJoinings.find({ userGroupId: recipientGroup.id, userId: Not(user.id) }); + for (const joining of joinings) { + if (freshMessage.reads.includes(joining.userId)) return; // 既読 + publishMainStream(joining.userId, 'unreadMessagingMessage', messageObj); + pushSw(joining.userId, 'unreadMessagingMessage', messageObj); + } } }, 2000); diff --git a/src/server/api/endpoints/messaging/messages/delete.ts b/src/server/api/endpoints/messaging/messages/delete.ts index 6a896cd8d..fb1bb42a5 100644 --- a/src/server/api/endpoints/messaging/messages/delete.ts +++ b/src/server/api/endpoints/messaging/messages/delete.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; -import { publishMessagingStream } from '../../../../../services/stream'; +import { publishMessagingStream, publishGroupMessagingStream } from '../../../../../services/stream'; import * as ms from 'ms'; import { ApiError } from '../../../error'; import { MessagingMessages } from '../../../../../models'; @@ -10,7 +10,7 @@ export const meta = { stability: 'stable', desc: { - 'ja-JP': '指定したメッセージを削除します。', + 'ja-JP': '指定したトークメッセージを削除します。', 'en-US': 'Delete a message.' }, @@ -57,6 +57,10 @@ export default define(meta, async (ps, user) => { await MessagingMessages.delete(message.id); - publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id); - publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id); + if (message.recipientId) { + publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id); + publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id); + } else if (message.groupId) { + publishGroupMessagingStream(message.groupId, 'deleted', message.id); + } }); diff --git a/src/server/api/endpoints/messaging/messages/read.ts b/src/server/api/endpoints/messaging/messages/read.ts index 50b7f3987..dd3449af1 100644 --- a/src/server/api/endpoints/messaging/messages/read.ts +++ b/src/server/api/endpoints/messaging/messages/read.ts @@ -1,13 +1,13 @@ import $ from 'cafy'; import { ID } from '../../../../../misc/cafy-id'; -import read from '../../../common/read-messaging-message'; import define from '../../../define'; import { ApiError } from '../../../error'; import { MessagingMessages } from '../../../../../models'; +import { readUserMessagingMessage, readGroupMessagingMessage } from '../../../common/read-messaging-message'; export const meta = { desc: { - 'ja-JP': '指定した自分宛てのメッセージを既読にします。', + 'ja-JP': '指定した自分宛てのトークメッセージを既読にします。', 'en-US': 'Mark as read a message of messaging.' }, @@ -39,12 +39,21 @@ export const meta = { export default define(meta, async (ps, user) => { const message = await MessagingMessages.findOne({ id: ps.messageId, - recipientId: user.id }); if (message == null) { throw new ApiError(meta.errors.noSuchMessage); } - read(user.id, message.userId, [message.id]); + if (message.recipientId) { + await readUserMessagingMessage(user.id, message.recipientId, [message.id]).catch(e => { + if (e.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError(meta.errors.noSuchMessage); + throw e; + }); + } else if (message.groupId) { + await readGroupMessagingMessage(user.id, message.groupId, [message.id]).catch(e => { + if (e.id === '930a270c-714a-46b2-b776-ad27276dc569') throw new ApiError(meta.errors.noSuchMessage); + throw e; + }); + } }); diff --git a/src/server/api/endpoints/users/groups/create.ts b/src/server/api/endpoints/users/groups/create.ts new file mode 100644 index 000000000..ee6cade8d --- /dev/null +++ b/src/server/api/endpoints/users/groups/create.ts @@ -0,0 +1,51 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { UserGroups, UserGroupJoinings } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; +import { UserGroup } from '../../../../../models/entities/user-group'; +import { types, bool } from '../../../../../misc/schema'; +import { UserGroupJoining } from '../../../../../models/entities/user-group-joining'; + +export const meta = { + desc: { + 'ja-JP': 'ユーザーグループを作成します。', + 'en-US': 'Create a user group.' + }, + + tags: ['groups'], + + requireCredential: true, + + kind: 'write:user-groups', + + params: { + name: { + validator: $.str.range(1, 100) + } + }, + + res: { + type: types.object, + optional: bool.false, nullable: bool.false, + ref: 'UserGroup', + }, +}; + +export default define(meta, async (ps, user) => { + const userGroup = await UserGroups.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + name: ps.name, + } as UserGroup); + + // Push the owner + await UserGroupJoinings.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + userGroupId: userGroup.id + } as UserGroupJoining); + + return await UserGroups.pack(userGroup); +}); diff --git a/src/server/api/endpoints/users/groups/delete.ts b/src/server/api/endpoints/users/groups/delete.ts new file mode 100644 index 000000000..4f89c324a --- /dev/null +++ b/src/server/api/endpoints/users/groups/delete.ts @@ -0,0 +1,49 @@ +import $ from 'cafy'; +import { ID } from '../../../../../misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { UserGroups } from '../../../../../models'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーグループを削除します。', + 'en-US': 'Delete a user group' + }, + + tags: ['groups'], + + requireCredential: true, + + kind: 'write:user-groups', + + params: { + groupId: { + validator: $.type(ID), + desc: { + 'ja-JP': '対象となるユーザーグループのID', + 'en-US': 'ID of target user group' + } + } + }, + + errors: { + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: '63dbd64c-cd77-413f-8e08-61781e210b38' + } + } +}; + +export default define(meta, async (ps, user) => { + const userGroup = await UserGroups.findOne({ + id: ps.groupId, + userId: user.id + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + await UserGroups.delete(userGroup.id); +}); diff --git a/src/server/api/endpoints/users/groups/joined.ts b/src/server/api/endpoints/users/groups/joined.ts new file mode 100644 index 000000000..14561fce0 --- /dev/null +++ b/src/server/api/endpoints/users/groups/joined.ts @@ -0,0 +1,33 @@ +import define from '../../../define'; +import { UserGroups, UserGroupJoinings } from '../../../../../models'; +import { types, bool } from '../../../../../misc/schema'; + +export const meta = { + desc: { + 'ja-JP': '自分の所属するユーザーグループ一覧を取得します。' + }, + + tags: ['groups', 'account'], + + requireCredential: true, + + kind: 'read:user-groups', + + res: { + type: types.array, + optional: bool.false, nullable: bool.false, + items: { + type: types.object, + optional: bool.false, nullable: bool.false, + ref: 'UserGroup', + } + }, +}; + +export default define(meta, async (ps, me) => { + const joinings = await UserGroupJoinings.find({ + userId: me.id, + }); + + return await Promise.all(joinings.map(x => UserGroups.pack(x.userGroupId))); +}); diff --git a/src/server/api/endpoints/users/groups/owned.ts b/src/server/api/endpoints/users/groups/owned.ts new file mode 100644 index 000000000..6cf39a142 --- /dev/null +++ b/src/server/api/endpoints/users/groups/owned.ts @@ -0,0 +1,33 @@ +import define from '../../../define'; +import { UserGroups } from '../../../../../models'; +import { types, bool } from '../../../../../misc/schema'; + +export const meta = { + desc: { + 'ja-JP': '自分の作成したユーザーグループ一覧を取得します。' + }, + + tags: ['groups', 'account'], + + requireCredential: true, + + kind: 'read:user-groups', + + res: { + type: types.array, + optional: bool.false, nullable: bool.false, + items: { + type: types.object, + optional: bool.false, nullable: bool.false, + ref: 'UserGroup', + } + }, +}; + +export default define(meta, async (ps, me) => { + const userGroups = await UserGroups.find({ + userId: me.id, + }); + + return await Promise.all(userGroups.map(x => UserGroups.pack(x))); +}); diff --git a/src/server/api/endpoints/users/groups/pull.ts b/src/server/api/endpoints/users/groups/pull.ts new file mode 100644 index 000000000..5fc0c2fa5 --- /dev/null +++ b/src/server/api/endpoints/users/groups/pull.ts @@ -0,0 +1,68 @@ +import $ from 'cafy'; +import { ID } from '../../../../../misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { getUser } from '../../../common/getters'; +import { UserGroups, UserGroupJoinings } from '../../../../../models'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーグループから指定したユーザーを削除します。', + 'en-US': 'Remove a user to a user group.' + }, + + tags: ['groups', 'users'], + + requireCredential: true, + + kind: 'write:user-groups', + + params: { + groupId: { + validator: $.type(ID), + }, + + userId: { + validator: $.type(ID), + desc: { + 'ja-JP': '対象のユーザーのID', + 'en-US': 'Target user ID' + } + }, + }, + + errors: { + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: '4662487c-05b1-4b78-86e5-fd46998aba74' + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '0b5cc374-3681-41da-861e-8bc1146f7a55' + } + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the group + const userGroup = await UserGroups.findOne({ + id: ps.groupId, + userId: me.id, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // Fetch the user + const user = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); + + // Pull the user + await UserGroupJoinings.delete({ userId: user.id }); +}); diff --git a/src/server/api/endpoints/users/groups/push.ts b/src/server/api/endpoints/users/groups/push.ts new file mode 100644 index 000000000..5371580db --- /dev/null +++ b/src/server/api/endpoints/users/groups/push.ts @@ -0,0 +1,90 @@ +import $ from 'cafy'; +import { ID } from '../../../../../misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { getUser } from '../../../common/getters'; +import { UserGroups, UserGroupJoinings } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; +import { UserGroupJoining } from '../../../../../models/entities/user-group-joining'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーグループに指定したユーザーを追加します。', + 'en-US': 'Add a user to a user group.' + }, + + tags: ['groups', 'users'], + + requireCredential: true, + + kind: 'write:user-groups', + + params: { + groupId: { + validator: $.type(ID), + }, + + userId: { + validator: $.type(ID), + desc: { + 'ja-JP': '対象のユーザーのID', + 'en-US': 'Target user ID' + } + }, + }, + + errors: { + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: '583f8bc0-8eee-4b78-9299-1e14fc91e409' + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: 'da52de61-002c-475b-90e1-ba64f9cf13a8' + }, + + alreadyAdded: { + message: 'That user has already been added to that group.', + code: 'ALREADY_ADDED', + id: '7e35c6a0-39b2-4488-aea6-6ee20bd5da2c' + } + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the group + const userGroup = await UserGroups.findOne({ + id: ps.groupId, + userId: me.id, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // Fetch the user + const user = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); + + const exist = await UserGroupJoinings.findOne({ + userGroupId: userGroup.id, + userId: user.id + }); + + if (exist) { + throw new ApiError(meta.errors.alreadyAdded); + } + + // Push the user + await UserGroupJoinings.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + userGroupId: userGroup.id + } as UserGroupJoining); +}); diff --git a/src/server/api/endpoints/users/groups/show.ts b/src/server/api/endpoints/users/groups/show.ts new file mode 100644 index 000000000..5f2c83988 --- /dev/null +++ b/src/server/api/endpoints/users/groups/show.ts @@ -0,0 +1,53 @@ +import $ from 'cafy'; +import { ID } from '../../../../../misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { UserGroups } from '../../../../../models'; +import { types, bool } from '../../../../../misc/schema'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーグループの情報を取得します。', + 'en-US': 'Show a user group.' + }, + + tags: ['groups', 'account'], + + requireCredential: true, + + kind: 'read:user-groups', + + params: { + groupId: { + validator: $.type(ID), + }, + }, + + res: { + type: types.object, + optional: bool.false, nullable: bool.false, + ref: 'UserGroup', + }, + + errors: { + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: 'ea04751e-9b7e-487b-a509-330fb6bd6b9b' + }, + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the group + const userGroup = await UserGroups.findOne({ + id: ps.groupId, + userId: me.id, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + return await UserGroups.pack(userGroup); +}); diff --git a/src/server/api/endpoints/users/lists/push.ts b/src/server/api/endpoints/users/lists/push.ts index 2763b3a19..bdc840308 100644 --- a/src/server/api/endpoints/users/lists/push.ts +++ b/src/server/api/endpoints/users/lists/push.ts @@ -80,5 +80,5 @@ export default define(meta, async (ps, me) => { } // Push the user - pushUserToUserList(user, userList); + await pushUserToUserList(user, userList); }); diff --git a/src/server/api/kinds.ts b/src/server/api/kinds.ts index 76d5a8a61..be3c30f7d 100644 --- a/src/server/api/kinds.ts +++ b/src/server/api/kinds.ts @@ -23,4 +23,6 @@ export const kinds = [ 'write:pages', 'write:page-likes', 'read:page-likes', + 'read:user-groups', + 'write:user-groups', ]; diff --git a/src/server/api/openapi/schemas.ts b/src/server/api/openapi/schemas.ts index 628bba511..32f69bdef 100644 --- a/src/server/api/openapi/schemas.ts +++ b/src/server/api/openapi/schemas.ts @@ -13,6 +13,7 @@ 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'; export function convertSchemaToOpenApiSchema(schema: Schema) { const res: any = schema; @@ -66,6 +67,7 @@ export const schemas = { User: convertSchemaToOpenApiSchema(packedUserSchema), UserList: convertSchemaToOpenApiSchema(packedUserListSchema), + UserGroup: convertSchemaToOpenApiSchema(packedUserGroupSchema), App: convertSchemaToOpenApiSchema(packedAppSchema), MessagingMessage: convertSchemaToOpenApiSchema(packedMessagingMessageSchema), Note: convertSchemaToOpenApiSchema(packedNoteSchema), diff --git a/src/server/api/stream/channels/messaging.ts b/src/server/api/stream/channels/messaging.ts index ce766e28e..1e5e94c1c 100644 --- a/src/server/api/stream/channels/messaging.ts +++ b/src/server/api/stream/channels/messaging.ts @@ -1,20 +1,39 @@ import autobind from 'autobind-decorator'; -import read from '../../common/read-messaging-message'; +import { readUserMessagingMessage, readGroupMessagingMessage } from '../../common/read-messaging-message'; import Channel from '../channel'; +import { UserGroupJoinings } from '../../../../models'; export default class extends Channel { public readonly chName = 'messaging'; public static shouldShare = false; public static requireCredential = true; - private otherpartyId: string; + private otherpartyId: string | null; + private groupId: string | null; @autobind public async init(params: any) { this.otherpartyId = params.otherparty as string; + this.groupId = params.group as string; + + // Check joining + if (this.groupId) { + const joining = await UserGroupJoinings.findOne({ + userId: this.user!.id, + userGroupId: this.groupId + }); + + if (joining == null) { + return; + } + } + + const subCh = this.otherpartyId + ? `messagingStream:${this.user!.id}-${this.otherpartyId}` + : `messagingStream:${this.groupId}`; // Subscribe messaging stream - this.subscriber.on(`messagingStream:${this.user!.id}-${this.otherpartyId}`, data => { + this.subscriber.on(subCh, data => { this.send(data); }); } @@ -23,7 +42,11 @@ export default class extends Channel { public onMessage(type: string, body: any) { switch (type) { case 'read': - read(this.user!.id, this.otherpartyId, [body.id]); + if (this.otherpartyId) { + readUserMessagingMessage(this.user!.id, this.otherpartyId, [body.id]); + } else if (this.groupId) { + readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]); + } break; } } diff --git a/src/services/stream.ts b/src/services/stream.ts index 28cb2057e..a47798eef 100644 --- a/src/services/stream.ts +++ b/src/services/stream.ts @@ -3,6 +3,7 @@ import { User } from '../models/entities/user'; import { Note } from '../models/entities/note'; import { UserList } from '../models/entities/user-list'; import { ReversiGame } from '../models/entities/games/reversi/game'; +import { UserGroup } from '../models/entities/user-group'; class Publisher { private publish = (channel: string, type: string | null, value?: any): void => { @@ -39,6 +40,10 @@ class Publisher { this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); } + public publishGroupMessagingStream = (groupId: UserGroup['id'], type: string, value?: any): void => { + this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value); + } + public publishMessagingIndexStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value); } @@ -74,6 +79,7 @@ export const publishNoteStream = publisher.publishNoteStream; export const publishNotesStream = publisher.publishNotesStream; export const publishUserListStream = publisher.publishUserListStream; export const publishMessagingStream = publisher.publishMessagingStream; +export const publishGroupMessagingStream = publisher.publishGroupMessagingStream; export const publishMessagingIndexStream = publisher.publishMessagingIndexStream; export const publishReversiStream = publisher.publishReversiStream; export const publishReversiGameStream = publisher.publishReversiGameStream; From 5a653531e26b9539a0382c3c9e51785c2bf4682e Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 18 May 2019 21:22:37 +0900 Subject: [PATCH 032/153] Avoid error --- src/client/app/common/views/deck/deck.column-template.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/app/common/views/deck/deck.column-template.vue b/src/client/app/common/views/deck/deck.column-template.vue index 09583de4b..592328516 100644 --- a/src/client/app/common/views/deck/deck.column-template.vue +++ b/src/client/app/common/views/deck/deck.column-template.vue @@ -1,7 +1,7 @@ @@ -16,8 +16,6 @@ import XColumn from './deck.column.vue'; import XNotes from './deck.notes.vue'; import { genSearchQuery } from '../../../common/scripts/gen-search-query'; -const limit = 20; - export default Vue.extend({ components: { XColumn, @@ -26,24 +24,11 @@ export default Vue.extend({ data() { return { - makePromise: async cursor => this.$root.api('notes/search', { - limit: limit + 1, - offset: cursor ? cursor : undefined, - ...(await genSearchQuery(this, this.q)) - }).then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - return { - notes: notes, - cursor: cursor ? cursor + limit : limit - }; - } else { - return { - notes: notes, - more: false - }; - } - }) + pagination: { + endpoint: 'notes/search', + limit: 20, + params: () => genSearchQuery(this, this.q) + } }; }, diff --git a/src/client/app/common/views/deck/deck.tl.vue b/src/client/app/common/views/deck/deck.tl.vue index 9284f06ee..e6c716070 100644 --- a/src/client/app/common/views/deck/deck.tl.vue +++ b/src/client/app/common/views/deck/deck.tl.vue @@ -6,7 +6,7 @@

    {{ $t('disabled-timeline.description') }}

    - + diff --git a/src/client/app/common/views/pages/featured.vue b/src/client/app/common/views/pages/featured.vue new file mode 100644 index 000000000..42c97e09f --- /dev/null +++ b/src/client/app/common/views/pages/featured.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/client/app/common/views/pages/followers.vue b/src/client/app/common/views/pages/followers.vue index 1d68d71e8..b546e69ae 100644 --- a/src/client/app/common/views/pages/followers.vue +++ b/src/client/app/common/views/pages/followers.vue @@ -1,7 +1,5 @@ + + diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue index b051ff51c..cb8b9c3ce 100644 --- a/src/client/app/desktop/views/components/notes.vue +++ b/src/client/app/desktop/views/components/notes.vue @@ -4,9 +4,9 @@
    -
    {{ $t('@.no-notes') }}
    +
    {{ $t('@.no-notes') }}
    - +