Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: 未読通知数を表示できるように #11982

Merged
merged 31 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8009ca8
未読通知数を表示できるように
kakkokari-gtyih Oct 7, 2023
e781b2e
Update Changelog
kakkokari-gtyih Oct 7, 2023
45680d7
オプトインにする
kakkokari-gtyih Oct 7, 2023
b1d1a78
Merge branch 'develop' into unread-notif-count
kakkokari-gtyih Oct 7, 2023
dc6944a
Fix lint
kakkokari-gtyih Oct 7, 2023
97261ec
Merge branch 'unread-notif-count' of https://github.com/kakkokari-gty…
kakkokari-gtyih Oct 7, 2023
b6065d2
(add) テスト通知のプッシュ通知を追加
kakkokari-gtyih Oct 7, 2023
b9a1dcf
add test
kakkokari-gtyih Oct 7, 2023
0e21f3e
Merge branch 'develop' into unread-notif-count
kakkokari-gtyih Oct 7, 2023
40435ca
フロントエンドの表示上限を99に変更
kakkokari-gtyih Oct 7, 2023
9c0e636
Merge branch 'unread-notif-count' of https://github.com/kakkokari-gty…
kakkokari-gtyih Oct 7, 2023
dae75d7
Merge branch 'develop' into unread-notif-count
kakkokari-gtyih Oct 8, 2023
20021f4
Merge branch 'develop' into unread-notif-count
kakkokari-gtyih Oct 8, 2023
3f715a1
Merge branch 'develop' into unread-notif-count
kakkokari-gtyih Oct 8, 2023
e322438
Merge branch 'develop' into unread-notif-count
kakkokari-gtyih Oct 8, 2023
a818cd5
Merge branch 'develop' into unread-notif-count
kakkokari-gtyih Oct 9, 2023
893e895
Merge remote-tracking branch 'origin/develop' into unread-notif-count
kakkokari-gtyih Oct 21, 2023
95c3618
Merge branch 'develop' into pr/11982
syuilo Oct 23, 2023
0ae9143
Merge branch 'develop' into unread-notif-count
kakkokari-gtyih Oct 23, 2023
0e7f9e8
Merge branch 'develop' into unread-notif-count
kakkokari-gtyih Oct 24, 2023
aab487f
Make it default on
kakkokari-gtyih Oct 24, 2023
7474ba6
共通スタイルをくくりだす
kakkokari-gtyih Oct 24, 2023
aba3819
Update Changelog
kakkokari-gtyih Oct 24, 2023
beaaf83
Merge branch 'develop' into unread-notif-count
kakkokari-gtyih Oct 25, 2023
a56fa75
Merge branch 'develop' into unread-notif-count
kakkokari-gtyih Oct 27, 2023
62b92c7
Merge branch 'develop' into unread-notif-count
kakkokari-gtyih Oct 28, 2023
8677e90
Merge branch 'develop' into pr/11982
syuilo Nov 1, 2023
51c9d79
tweak
syuilo Nov 1, 2023
42aec3c
Update UserEntityService.ts
syuilo Nov 1, 2023
c680870
rename
syuilo Nov 1, 2023
c9c5b23
Update navbar-for-mobile.vue
syuilo Nov 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- 最大でも黄色いエリア内にデコレーションを収めることを推奨します。
- 画像は512x512pxを推奨します。
- Enhance: すでにフォローしたすべての人の返信をTLに追加できるように
- Enhance: 未読の通知数を表示できるように
- Enhance: ローカリゼーションの更新
- Enhance: 依存関係の更新

Expand Down Expand Up @@ -63,6 +64,7 @@
- Fix: リノートをリノートできるのを修正
- Fix: アクセストークンを削除すると、通知が取得できなくなる場合がある問題を修正
- Fix: 自身の宛先なしダイレクト投稿がストリーミングで流れてこない問題を修正
- Fix: サーバーサイドからのテスト通知を正しく行えるように修正

## 2023.10.2

Expand Down
Binary file added packages/backend/assets/tabler-badges/bell.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion packages/backend/src/core/NotificationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ export class NotificationService implements OnApplicationShutdown {
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);

// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
// テスト通知の場合は即時発行
const interval = notification.type === 'test' ? 0 : 2000;
setTimeout(interval, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
if (latestReadNotificationId && (latestReadNotificationId >= (await redisIdPromise)!)) return;

Expand Down
39 changes: 30 additions & 9 deletions packages/backend/src/core/entities/UserEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js';
import { MiNotification } from '@/models/Notification.js';
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
Expand Down Expand Up @@ -235,17 +236,34 @@ export class UserEntityService implements OnModuleInit {
}

@bindThis
public async getHasUnreadNotification(userId: MiUser['id']): Promise<boolean> {
public async getNotificationsInfo(userId: MiUser['id']): Promise<{
hasUnread: boolean;
unreadCount: number;
}> {
const response = {
hasUnread: false,
unreadCount: 0,
};

const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);

const latestNotificationIdsRes = await this.redisClient.xrevrange(
`notificationTimeline:${userId}`,
'+',
'-',
'COUNT', 1);
const latestNotificationId = latestNotificationIdsRes[0]?.[0];
if (!latestReadNotificationId) {
response.unreadCount = await this.redisClient.xlen(`notificationTimeline:${userId}`);
} else {
const latestNotificationIdsRes = await this.redisClient.xrevrange(
`notificationTimeline:${userId}`,
'+',
latestReadNotificationId,
);

response.unreadCount = (latestNotificationIdsRes.length - 1 >= 0) ? latestNotificationIdsRes.length - 1 : 0;
}

return latestNotificationId != null && (latestReadNotificationId == null || latestReadNotificationId < latestNotificationId);
if (response.unreadCount > 0) {
response.hasUnread = true;
}

return response;
}

@bindThis
Expand Down Expand Up @@ -331,6 +349,8 @@ export class UserEntityService implements OnModuleInit {
...announcement,
})) : null;

const notificationsInfo = isMe && opts.detail ? await this.getNotificationsInfo(user.id) : null;

const packed = {
id: user.id,
name: user.name,
Expand Down Expand Up @@ -449,8 +469,9 @@ export class UserEntityService implements OnModuleInit {
unreadAnnouncements,
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
hasUnreadChannel: false, // 後方互換性のため
hasUnreadNotification: this.getHasUnreadNotification(user.id),
hasUnreadNotification: notificationsInfo?.hasUnread, // 後方互換性のため
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
unreadNotificationsCount: notificationsInfo?.unreadCount,
mutedWords: profile!.mutedWords,
mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: [], // 後方互換性のため
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/models/json-schema/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,10 @@ export const packedMeDetailedOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
unreadNotificationsCount: {
type: 'number',
nullable: false, optional: false,
},
mutedWords: {
type: 'array',
nullable: false, optional: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/test/e2e/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ describe('ユーザー', () => {
hasUnreadAntenna: user.hasUnreadAntenna,
hasUnreadChannel: user.hasUnreadChannel,
hasUnreadNotification: user.hasUnreadNotification,
unreadNotificationsCount: user.unreadNotificationsCount,
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
unreadAnnouncements: user.unreadAnnouncements,
mutedWords: user.mutedWords,
Expand Down Expand Up @@ -414,6 +415,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.hasUnreadAntenna, false);
assert.strictEqual(response.hasUnreadChannel, false);
assert.strictEqual(response.hasUnreadNotification, false);
assert.strictEqual(response.unreadNotificationsCount, 0);
assert.strictEqual(response.hasPendingReceivedFollowRequest, false);
assert.deepStrictEqual(response.unreadAnnouncements, []);
assert.deepStrictEqual(response.mutedWords, []);
Expand Down
11 changes: 9 additions & 2 deletions packages/frontend/src/boot/main-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,18 @@ export async function mainBoot() {
});

main.on('readAllNotifications', () => {
updateAccount({ hasUnreadNotification: false });
updateAccount({
hasUnreadNotification: false,
unreadNotificationsCount: 0,
});
});

main.on('unreadNotification', () => {
updateAccount({ hasUnreadNotification: true });
const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
updateAccount({
hasUnreadNotification: true,
unreadNotificationsCount,
});
});

main.on('unreadMention', () => {
Expand Down
22 changes: 18 additions & 4 deletions packages/frontend/src/components/MkLaunchPad.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal.close()" @closed="emit('closed')">
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
<div class="main">
<template v-for="item in items">
<template v-for="item in items" :key="item.text">
<button v-if="item.action" v-click-anime class="_button item" @click="$event => { item.action($event); close(); }">
<i class="icon" :class="item.icon"></i>
<div class="text">{{ item.text }}</div>
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
<span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
<span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
</button>
<MkA v-else v-click-anime :to="item.to" class="item" @click.passive="close()">
<i class="icon" :class="item.icon"></i>
<div class="text">{{ item.text }}</div>
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
<span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
<span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
</MkA>
</template>
</div>
Expand All @@ -27,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { } from 'vue';
import MkModal from '@/components/MkModal.vue';
import { navbarItemDef } from '@/navbar';
import { navbarItemDef } from '@/navbar.js';
import { defaultStore } from '@/store.js';
import { deviceKind } from '@/scripts/device-kind.js';

Expand Down Expand Up @@ -57,6 +59,7 @@ const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k =>
to: def.to,
action: def.action,
indicate: def.indicated,
indicateValue: def.indicateValue,
}));

function close() {
Expand Down Expand Up @@ -116,6 +119,17 @@ function close() {
line-height: 1.5em;
}

> .indicatorWithValue {
position: absolute;
top: 32px;
left: 16px;

@media (max-width: 500px) {
top: 16px;
left: 8px;
}
}

> .indicator {
position: absolute;
top: 32px;
Expand Down
6 changes: 5 additions & 1 deletion packages/frontend/src/components/MkNotifications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>

<script lang="ts" setup>
import { onUnmounted, onMounted, computed, shallowRef } from 'vue';
import { onUnmounted, onDeactivated, onMounted, computed, shallowRef } from 'vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
import XNotification from '@/components/MkNotification.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
Expand Down Expand Up @@ -68,6 +68,10 @@ onMounted(() => {
onUnmounted(() => {
if (connection) connection.dispose();
});

onDeactivated(() => {
if (connection) connection.dispose();
});
</script>

<style lang="scss" module>
Expand Down
9 changes: 9 additions & 0 deletions packages/frontend/src/navbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ export const navbarItemDef = reactive({
icon: 'ti ti-bell',
show: computed(() => $i != null),
indicated: computed(() => $i != null && $i.hasUnreadNotification),
indicateValue: computed(() => {
if (!$i || $i.unreadNotificationsCount === 0) return '';

if ($i.unreadNotificationsCount > 99) {
return '99+';
} else {
return $i.unreadNotificationsCount.toString();
}
}),
to: '/my/notifications',
},
drive: {
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/pages/settings/notifications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage
const userLists = await os.api('users/lists/list');

async function readAllUnreadNotes() {
await os.api('i/read-all-unread-notes');
await os.apiWithDialog('i/read-all-unread-notes');
}

async function readAllNotifications() {
await os.api('notifications/mark-all-as-read');
await os.apiWithDialog('notifications/mark-all-as-read');
}

async function updateReceiveConfig(type, value) {
Expand Down
13 changes: 13 additions & 0 deletions packages/frontend/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,19 @@ hr {
background: currentColor;
}

._indicateCounter {
display: inline-flex;
color: var(--fgOnAccent);
font-weight: 700;
background: var(--indicator);
height: 1.5em;
min-width: 1.5em;
align-items: center;
justify-content: center;
border-radius: 99rem;
padding: 0.3em 0.5em;
}

._noSelect {
user-select: none;
-webkit-user-select: none;
Expand Down
3 changes: 2 additions & 1 deletion packages/frontend/src/ui/_common_/common.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ let notifications = $ref<Misskey.entities.Notification[]>([]);

function onNotification(notification: Misskey.entities.Notification, isClient = false) {
if (document.visibilityState === 'visible') {
if (!isClient) {
if (!isClient && notification.type !== 'test') {
// サーバーサイドのテスト通知の際は自動で既読をつけない(テストできないので)
useStream().send('readNotification');
}

Expand Down
11 changes: 10 additions & 1 deletion packages/frontend/src/ui/_common_/navbar-for-mobile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="item === '-'" :class="$style.divider"></div>
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" class="_button" :class="[$style.item, { [$style.active]: navbarItemDef[item].active }]" :activeClass="$style.active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span>
<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator">
<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
<i v-else class="_indicatorCircle"></i>
</span>
</component>
</template>
<div :class="$style.divider"></div>
Expand Down Expand Up @@ -252,6 +255,12 @@ function more() {
color: var(--navIndicator);
font-size: 8px;
animation: blink 1s infinite;

&:has(.itemIndicateValueIcon) {
animation: none;
left: auto;
right: 20px;
}
}

.itemText {
Expand Down
22 changes: 20 additions & 2 deletions packages/frontend/src/ui/_common_/navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ SPDX-License-Identifier: AGPL-3.0-only
v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"
>
<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span>
<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator">
<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
<i v-else class="_indicatorCircle"></i>
</span>
</component>
</template>
<div :class="$style.divider"></div>
Expand Down Expand Up @@ -106,7 +109,7 @@ function more(ev: MouseEvent) {
<style lang="scss" module>
.root {
--nav-width: 250px;
--nav-icon-only-width: 72px;
--nav-icon-only-width: 80px;

flex: 0 0 var(--nav-width);
width: var(--nav-width);
Expand Down Expand Up @@ -312,6 +315,13 @@ function more(ev: MouseEvent) {
color: var(--navIndicator);
font-size: 8px;
animation: blink 1s infinite;

&:has(.itemIndicateValueIcon) {
animation: none;
left: auto;
right: 40px;
font-size: 10px;
}
}

.itemText {
Expand Down Expand Up @@ -475,6 +485,14 @@ function more(ev: MouseEvent) {
color: var(--navIndicator);
font-size: 8px;
animation: blink 1s infinite;

&:has(.itemIndicateValueIcon) {
animation: none;
top: 4px;
left: auto;
right: 4px;
font-size: 10px;
}
}
}
</style>
11 changes: 10 additions & 1 deletion packages/frontend/src/ui/classic.sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="item === '-'" class="divider"></div>
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
<i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="_indicatorCircle"></i></span>
<span v-if="navbarItemDef[item].indicated" class="indicator">
<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
<i v-else class="_indicatorCircle"></i>
</span>
</component>
</template>
<div class="divider"></div>
Expand Down Expand Up @@ -218,6 +221,12 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
color: var(--navIndicator);
font-size: 8px;
animation: blink 1s infinite;

&:has(.itemIndicateValueIcon) {
animation: none;
left: auto;
right: 20px;
}
}

&:hover {
Expand Down
Loading
Loading