表示する通知を種別ごとに設定できるように (#6647)

* ストリーミング以外は一通り実装

* ストリーミング分も適用

* 通知のグローバル設定をサーバーサイドに保存

* グローバル通知を使うようにしたら更新されなくなるのを修正

* サーバーサイド処理

* i/notifications のパラメーター includeTypes に空配列を渡すと全部の通知が来る問題を修正

* 全て有効/無効ボタンを実装

* Squashed commit of the following:

commit c3c111529e
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Wed Aug 19 22:29:04 2020 +0900

    12.47.0

commit 2dbab66cfe
Author: syuilo <Syuilotan@yahoo.co.jp>
Date:   Wed Aug 19 22:24:39 2020 +0900

    New Crowdin updates (#6617)

    * New translations ja-JP.yml (French)

    * New translations ja-JP.yml (Arabic)

    * New translations ja-JP.yml (French)

    * New translations ja-JP.yml (Spanish)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (Chinese Simplified)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (Spanish)

    * New translations ja-JP.yml (Chinese Simplified)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (Chinese Simplified)

    * New translations ja-JP.yml (Spanish)

    * New translations ja-JP.yml (Chinese Simplified)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (Spanish)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

    * 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 (Korean)

    * New translations ja-JP.yml (Spanish)

    * New translations ja-JP.yml (Chinese Traditional)

    * New translations ja-JP.yml (Chinese Traditional)

    * New translations ja-JP.yml (Chinese Traditional)

    * New translations ja-JP.yml (Chinese Traditional)

    * New translations ja-JP.yml (Chinese Traditional)

    * New translations ja-JP.yml (Chinese Traditional)

    * New translations ja-JP.yml (Chinese Traditional)

    * New translations ja-JP.yml (Chinese Traditional)

    * New translations ja-JP.yml (Chinese Traditional)

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (Korean)

    * New translations ja-JP.yml (Chinese Simplified)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (Spanish)

    * New translations ja-JP.yml (Arabic)

    * New translations ja-JP.yml (French)

    * New translations ja-JP.yml (Chinese Traditional)

    * New translations ja-JP.yml (Chinese Simplified)

    * New translations ja-JP.yml (Chinese Simplified)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (Chinese Traditional)

    * New translations ja-JP.yml (Japanese, Kansai)

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (Korean)

    * New translations ja-JP.yml (Chinese Simplified)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (Spanish)

    * New translations ja-JP.yml (Arabic)

    * New translations ja-JP.yml (French)

    * New translations ja-JP.yml (Chinese Traditional)

    * New translations ja-JP.yml (German)

    * New translations ja-JP.yml (English)

    * New translations ja-JP.yml (Chinese Simplified)

commit 01238d6b1a
Author: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Date:   Wed Aug 19 22:24:02 2020 +0900

    Update README.md [AUTOGEN] (#6593)

commit c34f302b1c
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Wed Aug 19 21:47:18 2020 +0900

    enhance(client): サーバーから切断されたときにダイアログで警告を表示できるように

commit 6870262f8d
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Wed Aug 19 17:52:11 2020 +0900

    enhance(client): Better element visible detection

commit c54d5e7040
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Wed Aug 19 17:51:31 2020 +0900

    fix(clinet): 誤字によりスクロールイベントリスナが解除されていなかったのを修正

commit 0ace009a54
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Tue Aug 18 22:52:54 2020 +0900

    fix(server): Prevent error when recieve non-json data from websocket

    Fix #6658

commit 48e8ee440b
Author: MeiMei <30769358+mei23@users.noreply.github.com>
Date:   Tue Aug 18 22:48:52 2020 +0900

    WebPのアニメーションが失われるのを修正 Fix #6625 (#6649)

commit 9855405b89
Author: syuilo <Syuilotan@yahoo.co.jp>
Date:   Tue Aug 18 22:44:21 2020 +0900

    Channel (#6621)

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wop

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * add notes

    * wip

    * wip

    * wip

    * wip

    * sound

    * wip

    * add kick_gaba2

    * wip

commit 122076e8ea
Author: MeiMei <30769358+mei23@users.noreply.github.com>
Date:   Sat Aug 15 04:27:19 2020 +0900

    Sign (request-target) Fix #6652 (#6656)

commit 7c5ac2cbb4
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Fri Aug 14 15:24:55 2020 +0900

    perf(server): Add isSensitive index to improve query performance

commit ccda2181c1
Author: MeiMei <30769358+mei23@users.noreply.github.com>
Date:   Fri Aug 14 00:54:33 2020 +0900

    GCSに大きいファイルがアップロードできないのを修正 Fix #6254 (#6648)

commit b5fe4ba9be
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Thu Aug 13 23:02:43 2020 +0900

    WIP: Improve admin dashboard

commit fd9c7d525a
Merge: 080574e13 ee0a44559
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Thu Aug 13 21:27:10 2020 +0900

    Merge branch 'develop' of https://github.com/syuilo/misskey into develop

commit 080574e13d
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Thu Aug 13 21:27:06 2020 +0900

    WIP: Improve admin dashboard

commit ee0a445590
Author: MeiMei <30769358+mei23@users.noreply.github.com>
Date:   Thu Aug 13 20:05:01 2020 +0900

    Option objectStorageSetPublicRead (#6645)

commit bb342c7601
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Thu Aug 13 19:56:46 2020 +0900

    WIP: Improve admin dashboard

commit ed17636fb9
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Thu Aug 13 17:58:16 2020 +0900

    WIP: Improve admin dashboard

commit c59d7d941a
Author: syuilo <Syuilotan@yahoo.co.jp>
Date:   Wed Aug 12 17:42:12 2020 +0900

    Update README.md

    Close #6644

commit 377377595a
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 20:23:51 2020 +0900

    enhance(client): Improve admin page

commit d63aef9963
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 13:55:00 2020 +0900

    chore(client): Fix style

commit e9b28fa3c0
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 13:00:10 2020 +0900

    chore(client): Design tweaks

commit be255dc583
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 12:42:51 2020 +0900

    chore(client): Design tweak

commit 18eb7c6087
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 12:31:22 2020 +0900

    chore(client): Design tweaks

commit cf29e69813
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 12:28:35 2020 +0900

    chore(client): Fix bug

commit 132da7e3c0
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 12:20:58 2020 +0900

    Update ja-JP.yml

commit 26df23bb64
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 12:18:02 2020 +0900

    chore(client): fix style

commit 76389ad619
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 12:15:58 2020 +0900

    chore(client): Design tweaks

commit 7cde8cfbf2
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 11:51:43 2020 +0900

    chore(client): Design tweaks

commit 4eb2ddac4e
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 11:24:30 2020 +0900

    chore(client): Design tweaks

commit dc51eef27c
Merge: bff8a23cb 9c5efb9da
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 10:38:00 2020 +0900

    Merge branch 'develop' of https://github.com/syuilo/misskey into develop

commit bff8a23cbc
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Mon Aug 10 10:37:57 2020 +0900

    chore(client): Design tweaks

commit 9c5efb9da0
Author: rinsuki <428rinsuki+git@gmail.com>
Date:   Mon Aug 10 01:33:01 2020 +0900

    Dockerのビルド時にgitを入れるように (#6639)

    917d3d0bd3 でgitの依存関係が追加されたのにgitが入っていないのでコケていた

commit 48b8320e5e
Author: rinsuki <428rinsuki+git@gmail.com>
Date:   Mon Aug 10 01:32:27 2020 +0900

    Fix #6637 (#6638)

    * Fix #6637

    * fix lint

commit 9b2ed96c1c
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Sun Aug 9 15:59:38 2020 +0900

    chore: Clean up

commit 69d9aa71f2
Author: syuilo <Syuilotan@yahoo.co.jp>
Date:   Sun Aug 9 15:51:02 2020 +0900

    Full view mode (#6636)

    * wuip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * wip

    * Update folder.vue

    * wip

    * Update size.ts

    * wip

    * wip

    * Update index.vue

    * wip

commit 13683780cd
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Sun Aug 9 13:49:44 2020 +0900

    ✌️

commit d780e5b251
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Sun Aug 9 13:46:19 2020 +0900

    enhance(client): ミュートされたノート数を表示するようにしたり

commit 917d3d0bd3
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Sat Aug 8 10:30:38 2020 +0900

    chore: Update dependencies 🚀

commit 4b19c53697
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Sat Aug 8 10:27:37 2020 +0900

    client: テーマコードをコピーできるようにしたり

commit 2d40a15d2b
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Fri Aug 7 11:27:37 2020 +0900

    refactor: Extract well-known services

commit 2bdcd22ad4
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Tue Aug 4 23:09:48 2020 +0900

    enhance(api): アクセストークンを作成する際、createdAtをlastUsedAtを揃えるようにして、未使用かどうかを判定できるように

commit f73a4e1304
Author: MeiMei <30769358+mei23@users.noreply.github.com>
Date:   Tue Aug 4 21:12:55 2020 +0900

    Update .dockerignore (#6620)

commit b265cdbd84
Author: Xeltica <7106976+Xeltica@users.noreply.github.com>
Date:   Mon Aug 3 13:40:32 2020 +0900

    Update CHANGELOG.md

commit a04d8b95c2
Author: Xeltica <7106976+Xeltica@users.noreply.github.com>
Date:   Mon Aug 3 13:40:13 2020 +0900

    Update CHANGELOG.md

commit 0e9a8c0cd4
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Sun Aug 2 13:59:05 2020 +0900

    fix(client): Message read state is not reactive

commit 5ae8a3c7e8
Author: syuilo <syuilotan@yahoo.co.jp>
Date:   Sun Aug 2 13:49:28 2020 +0900

    refactor

* fix: includeTypes 未指定時に通知が返ってこなくなるバグを修正

* 最適化とバグ修正

* 挙動を修正

* Update ja-JP.yml

* 不要なimportを削除

* ✌

* 不要なコードの削除

* Update notification-setting-window.vue

* Update notification-setting-window.vue

* 🎨

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
Xeltica 2020-08-22 10:06:17 +09:00 committed by GitHub
parent 6dac505af9
commit cd0f8a4ef9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 245 additions and 44 deletions

View File

@ -566,6 +566,10 @@ delayed: "遅延"
database: "データベース" database: "データベース"
channel: "チャンネル" channel: "チャンネル"
create: "作成" create: "作成"
notificationSetting: "通知設定"
notificationSettingDesc: "表示する通知の種別を選択してください。"
useGlobalSetting: "グローバル設定を使う"
useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。"
_serverDisconnectedBehavior: _serverDisconnectedBehavior:
reload: "自動でリロード" reload: "自動でリロード"
@ -1285,8 +1289,11 @@ _notification:
renote: "Renote" renote: "Renote"
quote: "引用" quote: "引用"
reaction: "リアクション" reaction: "リアクション"
pollVote: "投票" pollVote: "アンケートに投票された"
receiveFollowRequest: "フォローリクエスト" receiveFollowRequest: "フォロー申請を受け取った"
followRequestAccepted: "フォローが受理された"
groupInvited: "グループに招待された"
app: "連携アプリからの通知"
_deck: _deck:
alwaysShowMainColumn: "常にメインカラムを表示" alwaysShowMainColumn: "常にメインカラムを表示"

View File

@ -0,0 +1,16 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class IncludingNotificationTypes1597236229720 implements MigrationInterface {
name = 'IncludingNotificationTypes1597236229720'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "user_profile_includingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "includingNotificationTypes" "user_profile_includingnotificationtypes_enum" array`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "includingNotificationTypes"`);
await queryRunner.query(`DROP TYPE "user_profile_includingnotificationtypes_enum"`);
}
}

View File

@ -328,6 +328,10 @@ export default Vue.extend({
}, },
async onNotification(notification) { async onNotification(notification) {
const t = this.$store.state.i.includingNotificationTypes;
if (!!t && !t.includes(notification.type)) {
return;
}
if (document.visibilityState === 'visible') { if (document.visibilityState === 'visible') {
this.$root.stream.send('readNotification', { this.$root.stream.send('readNotification', {
id: notification.id id: notification.id

View File

@ -2,7 +2,7 @@
<x-column :column="column" :is-stacked="isStacked" :menu="menu"> <x-column :column="column" :is-stacked="isStacked" :menu="menu">
<template #header><fa :icon="faBell" style="margin-right: 8px;"/>{{ column.name }}</template> <template #header><fa :icon="faBell" style="margin-right: 8px;"/>{{ column.name }}</template>
<x-notifications/> <x-notifications :include-types="column.includingTypes"/>
</x-column> </x-column>
</template> </template>
@ -38,28 +38,14 @@ export default Vue.extend({
}, },
created() { created() {
if (this.column.notificationType == null) {
this.column.notificationType = 'all';
this.$store.commit('deviceUser/updateDeckColumn', this.column);
}
this.menu = [{ this.menu = [{
icon: faCog, icon: faCog,
text: this.$t('notificationType'), text: this.$t('notificationSetting'),
action: () => { action: async () => {
this.$root.dialog({ this.$root.new(await import('../notification-setting-window.vue').then(m => m.default), {
title: this.$t('notificationType'), includingTypes: this.column.includingTypes,
type: null, }).$on('ok', async ({ includingTypes }) => {
select: { this.$set(this.column, 'includingTypes', includingTypes);
items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({
value: x, text: this.$t(`_notification._types.${x}`)
}))
default: this.column.notificationType,
},
showCancelButton: true
}).then(({ canceled, result: type }) => {
if (canceled) return;
this.column.notificationType = type;
this.$store.commit('deviceUser/updateDeckColumn', this.column); this.$store.commit('deviceUser/updateDeckColumn', this.column);
}); });
} }

View File

@ -0,0 +1,98 @@
<template>
<x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()">
<template #header>{{ $t('notificationSetting') }}</template>
<div class="vv94n3oa">
<div v-if="showGlobalToggle">
<mk-switch v-model="useGlobalSetting">
{{ $t('useGlobalSetting') }}
<template #desc>{{ $t('useGlobalSettingDesc') }}</template>
</mk-switch>
</div>
<div v-if="!useGlobalSetting">
<mk-info>{{ $t('notificationSettingDesc') }}</mk-info>
<mk-button inline @click="disableAll">{{ $t('disableAll') }}</mk-button>
<mk-button inline @click="enableAll">{{ $t('enableAll') }}</mk-button>
<mk-switch v-for="type in notificationTypes" :key="type" v-model="typesMap[type]">{{ $t(`_notification._types.${type}`) }}</mk-switch>
</div>
</div>
</x-window>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue';
import XWindow from './window.vue';
import MkSwitch from './ui/switch.vue';
import MkInfo from './ui/info.vue';
import MkButton from './ui/button.vue';
import { notificationTypes } from '../../types';
export default Vue.extend({
components: {
XWindow,
MkSwitch,
MkInfo,
MkButton
},
props: {
includingTypes: {
// TODO: 調
type: Array as PropType<typeof notificationTypes[number][]>,
required: false,
default: null,
},
showGlobalToggle: {
type: Boolean,
required: false,
default: true,
}
},
data() {
return {
typesMap: {} as Record<typeof notificationTypes[number], boolean>,
useGlobalSetting: false,
notificationTypes,
};
},
created() {
this.useGlobalSetting = this.includingTypes === null && this.showGlobalToggle;
for (const type of this.notificationTypes) {
Vue.set(this.typesMap, type, this.includingTypes === null || this.includingTypes.includes(type));
}
},
methods: {
ok() {
const includingTypes = this.useGlobalSetting ? null : (Object.keys(this.typesMap) as typeof notificationTypes[number][])
.filter(type => this.typesMap[type]);
this.$emit('ok', { includingTypes });
this.$refs.window.close();
},
disableAll() {
for (const type in this.typesMap) {
this.typesMap[type as typeof notificationTypes[number]] = false;
}
},
enableAll() {
for (const type in this.typesMap) {
this.typesMap[type as typeof notificationTypes[number]] = true;
}
}
}
});
</script>
<style lang="scss" scoped>
.vv94n3oa {
> div {
border-top: solid 1px var(--divider);
padding: 24px;
}
}
</style>

View File

@ -17,11 +17,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue, { PropType } from 'vue';
import paging from '../scripts/paging'; import paging from '../scripts/paging';
import XNotification from './notification.vue'; import XNotification from './notification.vue';
import XList from './date-separated-list.vue'; import XList from './date-separated-list.vue';
import XNote from './note.vue'; import XNote from './note.vue';
import { notificationTypes } from '../../types';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -35,9 +36,10 @@ export default Vue.extend({
], ],
props: { props: {
type: { includeTypes: {
type: String, type: Array as PropType<typeof notificationTypes[number][]>,
required: false required: false,
default: null,
}, },
}, },
@ -48,15 +50,26 @@ export default Vue.extend({
endpoint: 'i/notifications', endpoint: 'i/notifications',
limit: 10, limit: 10,
params: () => ({ params: () => ({
includeTypes: this.type ? [this.type] : undefined includeTypes: this.allIncludeTypes || undefined,
}) })
}, },
}; };
}, },
computed: {
allIncludeTypes() {
return this.includeTypes ?? this.$store.state.i.includingNotificationTypes;
}
},
watch: { watch: {
type() { includeTypes() {
this.reload(); this.reload();
},
'$store.state.i.includingNotificationTypes'() {
if (this.includeTypes === null) {
this.reload();
}
} }
}, },
@ -71,16 +84,20 @@ export default Vue.extend({
methods: { methods: {
onNotification(notification) { onNotification(notification) {
if (document.visibilityState === 'visible') { //
const isMuted = !!this.allIncludeTypes && !this.allIncludeTypes.includes(notification.type);
if (isMuted || document.visibilityState === 'visible') {
this.$root.stream.send('readNotification', { this.$root.stream.send('readNotification', {
id: notification.id id: notification.id
}); });
} }
if (!isMuted) {
this.prepend({ this.prepend({
...notification, ...notification,
isRead: document.visibilityState === 'visible' isRead: document.visibilityState === 'visible'
}); });
}
}, },
noteUpdated(oldValue, newValue) { noteUpdated(oldValue, newValue) {

View File

@ -161,6 +161,11 @@ export default Vue.extend({
}, },
async onNotification(notification) { async onNotification(notification) {
const t = this.$store.state.i.includingNotificationTypes;
if (!!t && !t.includes(notification.type)) {
return;
}
if (document.visibilityState === 'visible') { if (document.visibilityState === 'visible') {
this.$root.stream.send('readNotification', { this.$root.stream.send('readNotification', {
id: notification.id id: notification.id
@ -170,7 +175,6 @@ export default Vue.extend({
notification notification
}); });
} }
this.$root.sound('notification'); this.$root.sound('notification');
}, },

View File

@ -22,6 +22,9 @@
<mk-button @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</mk-button> <mk-button @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</mk-button>
<mk-button @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</mk-button> <mk-button @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</mk-button>
</div> </div>
<div class="_content">
<mk-button @click="configure">{{ $t('notificationSetting') }}</mk-button>
</div>
</section> </section>
<x-import-export class="_vMargin"/> <x-import-export class="_vMargin"/>
@ -109,6 +112,24 @@ export default Vue.extend({
readAllNotifications() { readAllNotifications() {
this.$root.api('notifications/mark-all-as-read'); this.$root.api('notifications/mark-all-as-read');
}, },
async configure() {
this.$root.new(await import('../../components/notification-setting-window.vue').then(m => m.default), {
includingTypes: this.$store.state.i.includingNotificationTypes,
showGlobalToggle: false,
}).$on('ok', async ({ includingTypes: value }: any) => {
await this.$root.api('i/update', {
includingNotificationTypes: value,
}).then(i => {
this.$store.state.i.includingNotificationTypes = i.includingNotificationTypes;
}).catch(err => {
this.$root.dialog({
type: 'error',
text: err.message
});
});
});
}
} }
}); });
</script> </script>

View File

@ -21,6 +21,11 @@ export type FormItem = {
default: string | null; default: string | null;
hidden?: boolean; hidden?: boolean;
enum: string[]; enum: string[];
} | {
label?: string;
type: 'array';
default: unknown[] | null;
hidden?: boolean;
}; };
export type Form = Record<string, FormItem>; export type Form = Record<string, FormItem>;

View File

@ -1,15 +1,16 @@
<template> <template>
<mk-container :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true"> <mk-container :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true">
<template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template> <template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template>
<template #func><button @click="configure()" class="_button"><fa :icon="faCog"/></button></template>
<div> <div>
<x-notifications/> <x-notifications :include-types="props.includingTypes"/>
</div> </div>
</mk-container> </mk-container>
</template> </template>
<script lang="ts"> <script lang="ts">
import { faBell } from '@fortawesome/free-solid-svg-icons'; import { faBell, faCog } from '@fortawesome/free-solid-svg-icons';
import MkContainer from '../components/ui/container.vue'; import MkContainer from '../components/ui/container.vue';
import XNotifications from '../components/notifications.vue'; import XNotifications from '../components/notifications.vue';
import define from './define'; import define from './define';
@ -25,6 +26,11 @@ export default define({
type: 'number', type: 'number',
default: 300, default: 300,
}, },
includingTypes: {
type: 'array',
hidden: true,
default: null,
},
}) })
}).extend({ }).extend({
components: { components: {
@ -34,8 +40,19 @@ export default define({
data() { data() {
return { return {
faBell faBell, faCog
}; };
}, },
methods: {
async configure() {
this.$root.new(await import('../components/notification-setting-window.vue').then(m => m.default), {
includingTypes: this.props.includingTypes,
}).$on('ok', async ({ includingTypes }) => {
this.props.includingTypes = includingTypes;
this.save();
});
}
}
}); });
</script> </script>

View File

@ -2,6 +2,7 @@ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'type
import { id } from '../id'; import { id } from '../id';
import { User } from './user'; import { User } from './user';
import { Page } from './page'; import { Page } from './page';
import { notificationTypes } from '../../types';
@Entity() @Entity()
export class UserProfile { export class UserProfile {
@ -158,6 +159,13 @@ export class UserProfile {
}) })
public mutedWords: string[][]; public mutedWords: string[][];
@Column('enum', {
enum: notificationTypes,
array: true,
nullable: true,
})
public includingNotificationTypes: typeof notificationTypes[number][] | null;
//#region Denormalized fields //#region Denormalized fields
@Index() @Index()
@Column('varchar', { @Column('varchar', {

View File

@ -248,6 +248,7 @@ export class UserRepository extends Repository<User> {
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
integrations: profile!.integrations, integrations: profile!.integrations,
mutedWords: profile!.mutedWords, mutedWords: profile!.mutedWords,
includingNotificationTypes: profile?.includingNotificationTypes,
} : {}), } : {}),
...(opts.includeSecrets ? { ...(opts.includeSecrets ? {

View File

@ -44,12 +44,10 @@ export const meta = {
includeTypes: { includeTypes: {
validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])), validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])),
default: [] as string[]
}, },
excludeTypes: { excludeTypes: {
validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])), validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])),
default: [] as string[]
} }
}, },
@ -65,6 +63,14 @@ export const meta = {
}; };
export default define(meta, async (ps, user) => { export default define(meta, async (ps, user) => {
// includeTypes が空の場合はクエリしない
if (ps.includeTypes && ps.includeTypes.length === 0) {
return [];
}
// excludeTypes に全指定されている場合はクエリしない
if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) {
return [];
}
const followingQuery = Followings.createQueryBuilder('following') const followingQuery = Followings.createQueryBuilder('following')
.select('following.followeeId') .select('following.followeeId')
.where('following.followerId = :followerId', { followerId: user.id }); .where('following.followerId = :followerId', { followerId: user.id });
@ -91,9 +97,9 @@ export default define(meta, async (ps, user) => {
query.setParameters(followingQuery.getParameters()); query.setParameters(followingQuery.getParameters());
} }
if (ps.includeTypes!.length > 0) { if (ps.includeTypes?.length > 0) {
query.andWhere(`notification.type IN (:...includeTypes)`, { includeTypes: ps.includeTypes }); query.andWhere(`notification.type IN (:...includeTypes)`, { includeTypes: ps.includeTypes });
} else if (ps.excludeTypes!.length > 0) { } else if (ps.excludeTypes?.length > 0) {
query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes }); query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes });
} }

View File

@ -14,6 +14,7 @@ import { Users, DriveFiles, UserProfiles, Pages } from '../../../../models';
import { User } from '../../../../models/entities/user'; import { User } from '../../../../models/entities/user';
import { UserProfile } from '../../../../models/entities/user-profile'; import { UserProfile } from '../../../../models/entities/user-profile';
import { ensure } from '../../../../prelude/ensure'; import { ensure } from '../../../../prelude/ensure';
import { notificationTypes } from '../../../../types';
export const meta = { export const meta = {
desc: { desc: {
@ -147,6 +148,10 @@ export const meta = {
mutedWords: { mutedWords: {
validator: $.optional.arr($.arr($.str)) validator: $.optional.arr($.arr($.str))
}, },
includingNotificationTypes: {
validator: $.optional.arr($.str.or(notificationTypes as unknown as string[]))
},
}, },
errors: { errors: {
@ -201,6 +206,7 @@ export default define(meta, async (ps, user, token) => {
profileUpdates.mutedWords = ps.mutedWords; profileUpdates.mutedWords = ps.mutedWords;
profileUpdates.enableWordMute = ps.mutedWords.length > 0; profileUpdates.enableWordMute = ps.mutedWords.length > 0;
} }
if (ps.includingNotificationTypes !== undefined) profileUpdates.includingNotificationTypes = ps.includingNotificationTypes as typeof notificationTypes[number][];
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;

View File

@ -1,6 +1,6 @@
import { publishMainStream } from './stream'; import { publishMainStream } from './stream';
import pushSw from './push-notification'; import pushSw from './push-notification';
import { Notifications, Mutings } from '../models'; import { Notifications, Mutings, UserProfiles } from '../models';
import { genId } from '../misc/gen-id'; import { genId } from '../misc/gen-id';
import { User } from '../models/entities/user'; import { User } from '../models/entities/user';
import { Notification } from '../models/entities/notification'; import { Notification } from '../models/entities/notification';
@ -14,13 +14,18 @@ export async function createNotification(
return null; return null;
} }
const profile = await UserProfiles.findOne({ userId: notifieeId });
const isMuted = !profile?.includingNotificationTypes?.includes(type);
// Create notification // Create notification
const notification = await Notifications.save({ const notification = await Notifications.save({
id: genId(), id: genId(),
createdAt: new Date(), createdAt: new Date(),
notifieeId: notifieeId, notifieeId: notifieeId,
type: type, type: type,
isRead: false, // 相手がこの通知をミュートしているようなら、既読を予めつけておく
isRead: isMuted,
...data ...data
} as Partial<Notification>); } as Partial<Notification>);