Skip to content

Commit

Permalink
feat: Improve Push Notification (#7667)
Browse files Browse the repository at this point in the history
* clean up

* ev => data

* refactor

* clean up

* add type

* antenna

* channel

* fix

* add Packed type

* add PackedRef

* fix lint

* add emoji schema

* add reversiGame

* add reversiMatching

* remove signin schema (use Signin entity)

* add schemas refs, fix Packed type

* wip PackedHoge => Packed<'Hoge'>

* add Packed type

* note-reaction

* user

* user-group

* user-list

* note

* app, messaging-message

* notification

* drive-file

* drive-folder

* following

* muting

* blocking

* hashtag

* page

* app (with modifying schema)

* import user?

* channel

* antenna

* clip

* gallery-post

* emoji

* Packed

* reversi-matching

* update stream.ts

* #7769 (comment)

* fix lint

* clean up?

* add app

* fix

* nanka iroiro

* wip

* wip

* fix lint

* fix loginId

* fix

* refactor

* refactor

* remove follow action

* clean up

* Revert "remove follow action"

This reverts commit defbb41.

* Revert "clean up"

This reverts commit f94919c.

* remove fetch specification

* renoteの条件追加

* apiFetch => cli

* bypass fetch?

* fix

* refactor: use path alias

* temp: add submodule

* remove submodule

* enhane: unison-reloadに指定したパスに移動できるように

* null

* null

* feat: ログインするアカウントのIDをクエリ文字列で指定する機能

* null

* await?

* rename

* rename

* Update read.ts

* merge

* get-note-summary

* fix

* swパッケージに

* add missing packages

* fix getNoteSummary

* add webpack-cli

* ✌️

* remove plugins

* sw-inject分離したがテストしてない

* fix notification.vue

* remove a blank line

* disconnect intersection observer

* disconnect2

* fix notification.vue

* remove a blank line

* disconnect intersection observer

* disconnect2

* fix

* ✌️

* clean up config

* typesを戻した

* Update packages/client/src/components/notification.vue

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* disconnect

* oops

* Failed to load the script unexpectedly回避
sw.jsとlib.tsを分離してみた

* truncate notification

* Update packages/client/src/ui/_common_/common.vue

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* clean up

* clean up

* キャッシュ対策

* Truncate push notification message

* クライアントがあったらストリームに接続しているということなので通知しない判定の位置を修正

* components/drive-file-thumbnail.vue

* components/drive-select-dialog.vue

* components/drive-window.vue

* merge

* fix

* Service Workerのビルドにesbuildを使うようにする

* return createEmptyNotification()

* fix

* i18n.ts

* update

* ✌️

* remove ts-loader

* fix

* fix

* enhance: Service Workerを常に登録するように

* pollEnded

* URLをsw.jsに戻す

* clean up

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
  • Loading branch information
3 people authored Apr 30, 2022
1 parent 1c6d5dd commit 766559c
Show file tree
Hide file tree
Showing 47 changed files with 1,834 additions and 298 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
## 12.x.x (unreleased)
### Improvements
-
- API: notifications/readは配列でも受け付けるように
- /share のクエリでリプライやファイル等の情報を渡せるように
- ページロードエラーページにリロードボタンを追加
### Bugfixes
-
Expand Down
10 changes: 8 additions & 2 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ antennaExcludeKeywords: "除外キーワード"
antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
notifyAntenna: "新しいノートを通知する"
withFileAntenna: "ファイルが添付されたノートのみ"
enableServiceworker: "ServiceWorkerを有効にする"
enableServiceworker: "ブラウザへのプッシュ通知を有効にする"
antennaUsersDescription: "ユーザー名を改行で区切って指定します"
caseSensitive: "大文字小文字を区別する"
withReplies: "返信を含む"
Expand Down Expand Up @@ -1668,8 +1668,9 @@ _notification:
youWereFollowed: "フォローされました"
youReceivedFollowRequest: "フォローリクエストが来ました"
yourFollowRequestAccepted: "フォローリクエストが承認されました"
youWereInvitedToGroup: "グループに招待されました"
youWereInvitedToGroup: "{userName}があなたをグループに招待しました"
pollEnded: "アンケートの結果が出ました"
emptyPushNotificationMessage: "プッシュ通知の更新をしました"

_types:
all: "すべて"
Expand All @@ -1686,6 +1687,11 @@ _notification:
groupInvited: "グループに招待された"
app: "連携アプリからの通知"

_actions:
followBack: "フォローバック"
reply: "返信"
renote: "Renote"

_deck:
alwaysShowMainColumn: "常にメインカラムを表示"
columnAlign: "カラムの寄せ"
Expand Down
29 changes: 29 additions & 0 deletions packages/backend/src/server/api/common/read-messaging-message.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { publishMainStream, publishGroupMessagingStream } from '@/services/stream.js';
import { publishMessagingStream } from '@/services/stream.js';
import { publishMessagingIndexStream } from '@/services/stream.js';
import { pushNotification } from '@/services/push-notification.js';
import { User, IRemoteUser } from '@/models/entities/user.js';
import { MessagingMessage } from '@/models/entities/messaging-message.js';
import { MessagingMessages, UserGroupJoinings, Users } from '@/models/index.js';
Expand Down Expand Up @@ -50,6 +51,21 @@ export async function readUserMessagingMessage(
if (!await Users.getHasUnreadMessagingMessage(userId)) {
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
publishMainStream(userId, 'readAllMessagingMessages');
pushNotification(userId, 'readAllMessagingMessages', undefined);
} else {
// そのユーザーとのメッセージで未読がなければイベント発行
const count = await MessagingMessages.count({
where: {
userId: otherpartyId,
recipientId: userId,
isRead: false,
},
take: 1
});

if (!count) {
pushNotification(userId, 'readAllMessagingMessagesOfARoom', { userId: otherpartyId });
}
}
}

Expand Down Expand Up @@ -104,6 +120,19 @@ export async function readGroupMessagingMessage(
if (!await Users.getHasUnreadMessagingMessage(userId)) {
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
publishMainStream(userId, 'readAllMessagingMessages');
pushNotification(userId, 'readAllMessagingMessages', undefined);
} else {
// そのグループにおいて未読がなければイベント発行
const unreadExist = await MessagingMessages.createQueryBuilder('message')
.where(`message.groupId = :groupId`, { groupId: groupId })
.andWhere('message.userId != :userId', { userId: userId })
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
.andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない
.getOne().then(x => x != null);

if (!unreadExist) {
pushNotification(userId, 'readAllMessagingMessagesOfARoom', { groupId });
}
}
}

Expand Down
26 changes: 14 additions & 12 deletions packages/backend/src/server/api/common/read-notification.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { publishMainStream } from '@/services/stream.js';
import { pushNotification } from '@/services/push-notification.js';
import { User } from '@/models/entities/user.js';
import { Notification } from '@/models/entities/notification.js';
import { Notifications, Users } from '@/models/index.js';
Expand All @@ -16,28 +17,29 @@ export async function readNotification(
isRead: true,
});

post(userId);
if (!await Users.getHasUnreadNotification(userId)) return postReadAllNotifications(userId);
else return postReadNotifications(userId, notificationIds);
}

export async function readNotificationByQuery(
userId: User['id'],
query: Record<string, any>
) {
// Update documents
await Notifications.update({
const notificationIds = await Notifications.find({
...query,
notifieeId: userId,
isRead: false,
}, {
isRead: true,
});
}).then(notifications => notifications.map(notification => notification.id));

return readNotification(userId, notificationIds);
}

post(userId);
function postReadAllNotifications(userId: User['id']) {
publishMainStream(userId, 'readAllNotifications');
return pushNotification(userId, 'readAllNotifications', undefined);
}

async function post(userId: User['id']) {
if (!await Users.getHasUnreadNotification(userId)) {
// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
publishMainStream(userId, 'readAllNotifications');
}
function postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) {
publishMainStream(userId, 'readNotifications', notificationIds);
return pushNotification(userId, 'readNotifications', { notificationIds });
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { publishMainStream } from '@/services/stream.js';
import { pushNotification } from '@/services/push-notification.js';
import define from '../../define.js';
import { Notifications } from '@/models/index.js';

Expand Down Expand Up @@ -28,4 +29,5 @@ export default define(meta, paramDef, async (ps, user) => {

// 全ての通知を読みましたよというイベントを発行
publishMainStream(user.id, 'readAllNotifications');
pushNotification(user.id, 'readAllNotifications', undefined);
});
41 changes: 23 additions & 18 deletions packages/backend/src/server/api/endpoints/notifications/read.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { publishMainStream } from '@/services/stream.js';
import define from '../../define.js';
import { Notifications } from '@/models/index.js';
import { readNotification } from '../../common/read-notification.js';
import { ApiError } from '../../error.js';

export const meta = {
desc: {
'ja-JP': '通知を既読にします。',
'en-US': 'Mark a notification as read.'
},

tags: ['notifications', 'account'],

requireCredential: true,
Expand All @@ -21,23 +23,26 @@ export const meta = {
} as const;

export const paramDef = {
type: 'object',
properties: {
notificationId: { type: 'string', format: 'misskey:id' },
},
required: ['notificationId'],
oneOf: [
{
type: 'object',
properties: {
notificationId: { type: 'string', format: 'misskey:id' },
},
required: ['notificationId'],
},
{
type: 'object',
properties: {
notificationIds: { type: 'array', items: { type: 'string', format: 'misskey:id' } },
},
required: ['notificationIds'],
},
],
} as const;

// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
const notification = await Notifications.findOneBy({
notifieeId: user.id,
id: ps.notificationId,
});

if (notification == null) {
throw new ApiError(meta.errors.noSuchNotification);
}

readNotification(user.id, [notification.id]);
if ('notificationId' in ps) return readNotification(user.id, [ps.notificationId]);
return readNotification(user.id, ps.notificationIds);
});
8 changes: 5 additions & 3 deletions packages/backend/src/server/web/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const _dirname = dirname(_filename);
const staticAssets = `${_dirname}/../../../assets/`;
const clientAssets = `${_dirname}/../../../../client/assets/`;
const assets = `${_dirname}/../../../../../built/_client_dist_/`;
const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;

// Init app
const app = new Koa();
Expand Down Expand Up @@ -136,9 +137,10 @@ router.get('/twemoji/(.*)', async ctx => {
});

// ServiceWorker
router.get('/sw.js', async ctx => {
await send(ctx as any, `/sw.${config.version}.js`, {
root: assets,
router.get(`/sw.js`, async ctx => {
await send(ctx as any, `/sw.js`, {
root: swAssets,
maxage: ms('10 minutes'),
});
});

Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/services/create-notification.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { publishMainStream } from '@/services/stream.js';
import pushSw from './push-notification.js';
import { pushNotification } from '@/services/push-notification.js';
import { Notifications, Mutings, UserProfiles, Users } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
import { User } from '@/models/entities/user.js';
Expand Down Expand Up @@ -52,8 +52,8 @@ export async function createNotification(
//#endregion

publishMainStream(notifieeId, 'unreadNotification', packed);
pushNotification(notifieeId, 'notification', packed);

pushSw(notifieeId, 'notification', packed);
if (type === 'follow') sendEmailNotification.follow(notifieeId, await Users.findOneByOrFail({ id: data.notifierId! }));
if (type === 'receiveFollowRequest') sendEmailNotification.receiveFollowRequest(notifieeId, await Users.findOneByOrFail({ id: data.notifierId! }));
}, 2000);
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/services/messages/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { MessagingMessages, UserGroupJoinings, Mutings, Users } from '@/models/i
import { genId } from '@/misc/gen-id.js';
import { MessagingMessage } from '@/models/entities/messaging-message.js';
import { publishMessagingStream, publishMessagingIndexStream, publishMainStream, publishGroupMessagingStream } from '@/services/stream.js';
import pushNotification from '../push-notification.js';
import { pushNotification } from '@/services/push-notification.js';
import { Not } from 'typeorm';
import { Note } from '@/models/entities/note.js';
import renderNote from '@/remote/activitypub/renderer/note.js';
Expand Down
24 changes: 15 additions & 9 deletions packages/backend/src/services/push-notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
import { Packed } from '@/misc/schema.js';
import { getNoteSummary } from '@/misc/get-note-summary.js';

type notificationType = 'notification' | 'unreadMessagingMessage';
type notificationBody = Packed<'Notification'> | Packed<'MessagingMessage'>;
// Defined also packages/sw/types.ts#L14-L21
type pushNotificationsTypes = {
'notification': Packed<'Notification'>;
'unreadMessagingMessage': Packed<'MessagingMessage'>;
'readNotifications': { notificationIds: string[] };
'readAllNotifications': undefined;
'readAllMessagingMessages': undefined;
'readAllMessagingMessagesOfARoom': { userId: string } | { groupId: string };
};

// プッシュメッセージサーバーには文字数制限があるため、内容を削減します
function truncateNotification(notification: Packed<'Notification'>): any {
Expand All @@ -17,20 +24,19 @@ function truncateNotification(notification: Packed<'Notification'>): any {
...notification.note,
// textをgetNoteSummaryしたものに置き換える
text: getNoteSummary(notification.type === 'renote' ? notification.note.renote as Packed<'Note'> : notification.note),
...{
cw: undefined,
reply: undefined,
renote: undefined,
user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる
}

cw: undefined,
reply: undefined,
renote: undefined,
user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる
}
};
}

return notification;
}

export default async function(userId: string, type: notificationType, body: notificationBody) {
export async function pushNotification<T extends keyof pushNotificationsTypes>(userId: string, type: T, body: pushNotificationsTypes[T]) {
const meta = await fetchMeta();

if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
Expand Down
6 changes: 5 additions & 1 deletion packages/client/src/components/notification.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
</template>

<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted } from 'vue';
import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue';
import * as misskey from 'misskey-js';
import { getNoteSummary } from '@/scripts/get-note-summary';
import XReactionIcon from './reaction-icon.vue';
Expand Down Expand Up @@ -126,6 +126,10 @@ export default defineComponent({
const connection = stream.useChannel('main');
connection.on('readAllNotifications', () => readObserver.disconnect());
watch(props.notification.isRead, () => {
readObserver.disconnect();
});
onUnmounted(() => {
readObserver.disconnect();
connection.dispose();
Expand Down
25 changes: 25 additions & 0 deletions packages/client/src/components/notifications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,31 @@ const onNotification = (notification) => {
onMounted(() => {
const connection = stream.useChannel('main');
connection.on('notification', onNotification);
connection.on('readAllNotifications', () => {
if (pagingComponent.value) {
for (const item of pagingComponent.value.queue) {
item.isRead = true;
}
for (const item of pagingComponent.value.items) {
item.isRead = true;
}
}
});
connection.on('readNotifications', notificationIds => {
if (pagingComponent.value) {
for (let i = 0; i < pagingComponent.value.queue.length; i++) {
if (notificationIds.includes(pagingComponent.value.queue[i].id)) {
pagingComponent.value.queue[i].isRead = true;
}
}
for (let i = 0; i < (pagingComponent.value.items || []).length; i++) {
if (notificationIds.includes(pagingComponent.value.items[i].id)) {
pagingComponent.value.items[i].isRead = true;
}
}
}
});
onUnmounted(() => {
connection.dispose();
});
Expand Down
1 change: 1 addition & 0 deletions packages/client/src/components/ui/pagination.vue
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ onDeactivated(() => {
defineExpose({
items,
queue,
backed,
reload,
fetchMoreAhead,
Expand Down
1 change: 0 additions & 1 deletion packages/client/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ if ($i && $i.token) {
try {
document.body.innerHTML = '<div>Please wait...</div>';
await login(i);
location.reload();
} catch (e) {
// Render the error screen
// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
Expand Down
3 changes: 3 additions & 0 deletions packages/client/src/scripts/get-user-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function(user: { name?: string | null, username: string }): string {
return user.name || user.username;
}
Loading

0 comments on commit 766559c

Please sign in to comment.