From 58ad5276ef9af36238d27232e5c5dda61a71a81d Mon Sep 17 00:00:00 2001 From: mattyatea Date: Tue, 6 Oct 2026 17:47:14 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88?= =?UTF-8?q?=E5=8F=97=E5=8F=96=E6=99=82=E3=81=AB=E8=B4=88=E3=82=8A=E4=B8=BB?= =?UTF-8?q?=E3=81=A8=E5=8F=97=E5=8F=96=E9=87=8F=E3=82=92=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ja-JP.yml | 3 ++ packages/backend/src/core/PointService.ts | 7 +++ .../entities/NotificationEntityService.ts | 2 +- packages/backend/src/models/Notification.ts | 6 +++ .../src/models/json-schema/notification.ts | 24 +++++++++ .../backend/src/models/json-schema/user.ts | 1 + .../src/server/api/endpoints/i/update.ts | 1 + packages/backend/src/types.ts | 1 + packages/backend/test/unit/PointService.ts | 49 +++++++++++++++++++ .../src/components/MkNotification.vue | 9 ++-- .../unit/point-received-notification.test.ts | 42 ++++++++++++++++ packages/i18n/src/autogen/locale.ts | 12 +++++ packages/misskey-js/etc/misskey-js.api.md | 2 +- packages/misskey-js/src/autogen/types.ts | 37 ++++++++++++-- packages/misskey-js/src/consts.ts | 1 + .../sw/src/scripts/create-notification.ts | 10 ++++ 16 files changed, 198 insertions(+), 9 deletions(-) create mode 100644 packages/backend/test/unit/PointService.ts create mode 100644 packages/frontend/test/unit/point-received-notification.test.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c3789c3900b..3a61edcbeca 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -3259,6 +3259,8 @@ _notification: flushNotification: "通知の履歴をリセットする" exportOfXCompleted: "{x}のエクスポートが完了しました" login: "ログインがありました" + pointReceived: "{points}{pointName}を受け取りました" + pointReceivedFrom: "{name}さんから{points}{pointName}を受け取りました" loginBonus: "ログインボーナス" createToken: "アクセストークンが作成されました" createTokenDescription: "心当たりがない場合は「{text}」を通じてアクセストークンを削除してください。" @@ -3282,6 +3284,7 @@ _notification: achievementEarned: "実績の獲得" exportCompleted: "エクスポートが完了した" login: "ログイン" + pointReceived: "ポイントを受け取った" loginBonus: "ログインボーナス" createToken: "アクセストークンの作成" test: "通知のテスト" diff --git a/packages/backend/src/core/PointService.ts b/packages/backend/src/core/PointService.ts index 693d377b05d..b1173548a08 100644 --- a/packages/backend/src/core/PointService.ts +++ b/packages/backend/src/core/PointService.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { UsersRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; @Injectable() @@ -14,6 +15,8 @@ export class PointService { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, + + private notificationService: NotificationService, ) { } @@ -62,6 +65,10 @@ export class PointService { points: newRecipientBalance, }); + this.notificationService.createNotification(recipientId, 'pointReceived', { + points: amount, + }, senderId); + return { success: true, senderBalance: newSenderBalance, diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 65930c3d690..f28251a98f8 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -194,7 +194,7 @@ export class NotificationEntityService implements OnModuleInit { header: notification.customHeader, icon: notification.customIcon, } : {}), - ...(notification.type === 'loginBonus' ? { + ...(notification.type === 'loginBonus' || notification.type === 'pointReceived' ? { points: notification.points, } : {}), }); diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index e7ddae29026..8a195a0bfae 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -142,6 +142,12 @@ export type MiNotification = { type: 'test'; id: string; createdAt: string; +} | { + type: 'pointReceived'; + id: string; + createdAt: string; + notifierId: MiUser['id']; + points: number; } | { type: 'loginBonus'; id: string; diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 30e9c9327a1..0a001b52a27 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -237,6 +237,30 @@ export const packedNotificationSchema = { optional: false, nullable: false, }, }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['pointReceived'], + }, + user: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, + }, + userId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + points: { + type: 'number', + optional: false, nullable: false, + }, + }, }, { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 3e442eccc5f..48f36f35093 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -629,6 +629,7 @@ export const packedMeDetailedOnlySchema = { test: { optional: true, ...notificationRecieveConfig }, login: { optional: true, ...notificationRecieveConfig }, createToken: { optional: true, ...notificationRecieveConfig }, + pointReceived: { optional: true, ...notificationRecieveConfig }, exportCompleted: { optional: true, ...notificationRecieveConfig }, }, }, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 035234ea4aa..0b066f19503 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -216,6 +216,7 @@ export const paramDef = { roleAssigned: notificationRecieveConfig, chatRoomInvitationReceived: notificationRecieveConfig, achievementEarned: notificationRecieveConfig, + pointReceived: notificationRecieveConfig, app: notificationRecieveConfig, test: notificationRecieveConfig, }, diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index a91a766482d..438f1314dac 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -44,6 +44,7 @@ export const notificationTypes = [ 'exportCompleted', 'login', 'createToken', + 'pointReceived', 'app', 'test', 'loginBonus', diff --git a/packages/backend/test/unit/PointService.ts b/packages/backend/test/unit/PointService.ts new file mode 100644 index 00000000000..ae74a688a73 --- /dev/null +++ b/packages/backend/test/unit/PointService.ts @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { beforeEach, describe, expect, test } from 'vitest'; +import { mockDeep } from 'vitest-mock-extended'; +import type { UsersRepository, MiUser } from '@/models/_.js'; +import { PointService } from '@/core/PointService.js'; +import { NotificationService } from '@/core/NotificationService.js'; + +describe('PointService', () => { + const usersRepository = mockDeep(); + const notificationService = mockDeep(); + const service = new PointService(usersRepository, notificationService); + + beforeEach(() => { + usersRepository.findOneByOrFail.mockReset(); + usersRepository.update.mockReset(); + notificationService.createNotification.mockReset(); + usersRepository.findOneByOrFail.mockResolvedValueOnce({ id: 'sender', points: 100 } as MiUser); + usersRepository.findOneByOrFail.mockResolvedValueOnce({ id: 'recipient', points: 20 } as MiUser); + }); + + test('notifies the recipient of the sender and amount after transferring points', async () => { + const result = await service.sendPoints('sender', 'recipient', 30); + + expect(result).toEqual({ success: true, senderBalance: 70, recipientBalance: 50 }); + expect(usersRepository.update).toHaveBeenCalledWith('sender', { points: 70 }); + expect(usersRepository.update).toHaveBeenCalledWith('recipient', { points: 50 }); + expect(notificationService.createNotification).toHaveBeenCalledExactlyOnceWith( + 'recipient', 'pointReceived', { points: 30 }, 'sender', + ); + expect(notificationService.createNotification.mock.invocationCallOrder[0]).toBeGreaterThan( + usersRepository.update.mock.invocationCallOrder[1], + ); + }); + + test('does not notify when the sender has insufficient points', async () => { + await expect(service.sendPoints('sender', 'recipient', 101)).rejects.toThrow('Insufficient points'); + expect(notificationService.createNotification).not.toHaveBeenCalled(); + }); + + test('does not notify when the balance update fails', async () => { + usersRepository.update.mockRejectedValueOnce(new Error('update failed')); + await expect(service.sendPoints('sender', 'recipient', 30)).rejects.toThrow('update failed'); + expect(notificationService.createNotification).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 643383368c2..c33bb95634f 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.t_scheduledNotePosted]: notification.type === 'scheduledNotePosted', [$style.t_scheduledNotePostFailed]: notification.type === 'scheduledNotePostFailed', [$style.t_achievementEarned]: notification.type === 'achievementEarned', - [$style.t_loginBonus]: notification.type === 'loginBonus', + [$style.t_loginBonus]: notification.type === 'loginBonus' || notification.type === 'pointReceived', [$style.t_exportCompleted]: notification.type === 'exportCompleted', [$style.t_login]: notification.type === 'login', [$style.t_createToken]: notification.type === 'createToken', @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -77,7 +77,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._notification.createToken }} {{ i18n.ts._notification.testNotification }} {{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }} - + {{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }} {{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }} {{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }} @@ -126,6 +126,9 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._achievements._types[`_${notification.achievement}`].title }} +
+ {{ i18n.tsx._notification.pointReceived({ points: notification.points, pointName: instance.pointName ?? i18n.ts.point }) }} +
+{{ notification.points }} {{ instance.pointName ?? i18n.ts.point }}
diff --git a/packages/frontend/test/unit/point-received-notification.test.ts b/packages/frontend/test/unit/point-received-notification.test.ts new file mode 100644 index 00000000000..4db70c14f0d --- /dev/null +++ b/packages/frontend/test/unit/point-received-notification.test.ts @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { afterEach, expect, test, vi } from 'vitest'; +import { cleanup, render } from '@testing-library/vue'; +import type * as Misskey from 'misskey-js'; +import MkNotification from '@/components/MkNotification.vue'; +import { instance } from '@/instance.js'; +import { i18n } from '@/i18n.js'; + +vi.mock('@/i.js', () => ({ ensureSignin: () => ({ id: 'recipient' }) })); +vi.mock('@/instance.js', () => ({ instance: { pointName: 'コイン' } })); +vi.mock('@/utility/misskey-api.js', () => ({ misskeyApi: vi.fn() })); +vi.mock('@/components/MkReactionIcon.vue', () => ({ default: { template: '' } })); +vi.mock('@/components/MkButton.vue', () => ({ default: { template: '' } })); + +afterEach(cleanup); + +test.each(['コイン', null])('shows the sender profile and received amount with point name %s', (pointName) => { + instance.pointName = pointName; + const notification = { + id: 'gift', createdAt: '2026-10-06T00:00:00.000Z', type: 'pointReceived', points: 30, + userId: 'sender', user: { id: 'sender', name: '贈り主', username: 'sender', host: null }, + } as Misskey.entities.Notification; + const view = render(MkNotification, { + props: { notification }, + global: { + stubs: { + MkA: { props: ['to'], template: '' }, + MkUserName: { props: ['user'], template: '{{ user.name }}' }, + MkAvatar: { props: ['user'], template: '' }, + }, + directives: { 'user-preview': {} }, + }, + }); + + expect(view.getByRole('link', { name: '贈り主' }).getAttribute('href')).toBe('/@sender'); + expect(view.container.querySelector('[data-avatar-user="sender"]')).not.toBeNull(); + expect(view.getByText(i18n.tsx._notification.pointReceived({ points: 30, pointName: pointName ?? i18n.ts.point }))).toBeTruthy(); +}); diff --git a/packages/i18n/src/autogen/locale.ts b/packages/i18n/src/autogen/locale.ts index 9cb14ed5781..3269c10b86b 100644 --- a/packages/i18n/src/autogen/locale.ts +++ b/packages/i18n/src/autogen/locale.ts @@ -12397,6 +12397,14 @@ export interface Locale extends ILocale { * ログインがありました */ "login": string; + /** + * {points}{pointName}を受け取りました + */ + "pointReceived": ParameterizedString<"points" | "pointName">; + /** + * {name}さんから{points}{pointName}を受け取りました + */ + "pointReceivedFrom": ParameterizedString<"name" | "points" | "pointName">; /** * ログインボーナス */ @@ -12482,6 +12490,10 @@ export interface Locale extends ILocale { * ログイン */ "login": string; + /** + * ポイントを受け取った + */ + "pointReceived": string; /** * ログインボーナス */ diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index d92b22ea75e..dfc65dcf144 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -3825,7 +3825,7 @@ type Notification_2 = components['schemas']['Notification']; type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json']; // @public (undocumented) -export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollEnded", "scheduledNotePosted", "scheduledNotePostFailed", "receiveFollowRequest", "followRequestAccepted", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned", "exportCompleted", "test", "login", "createToken"]; +export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollEnded", "scheduledNotePosted", "scheduledNotePostFailed", "receiveFollowRequest", "followRequestAccepted", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned", "exportCompleted", "test", "login", "createToken", "pointReceived"]; // @public (undocumented) export function nyaize(text: string): string; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 088ad1b4dde..07c5ba8e1c4 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -5011,6 +5011,15 @@ export type components = { /** Format: misskey:id */ userListId: string; }; + pointReceived?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; + } | { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }; exportCompleted?: { /** @enum {string} */ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; @@ -5376,6 +5385,17 @@ export type components = { /** @enum {string} */ type: 'scheduledNotePostFailed'; noteDraft: components['schemas']['NoteDraft']; + } | { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** @enum {string} */ + type: 'pointReceived'; + user: components['schemas']['UserLite']; + /** Format: id */ + userId: string; + points: number; } | { /** Format: id */ id: string; @@ -32500,8 +32520,8 @@ export interface operations { untilDate?: number; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'loginBonus' | 'pollVote' | 'groupInvited')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'loginBonus' | 'pollVote' | 'groupInvited')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'pointReceived' | 'app' | 'test' | 'loginBonus' | 'pollVote' | 'groupInvited')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'pointReceived' | 'app' | 'test' | 'loginBonus' | 'pollVote' | 'groupInvited')[]; }; }; }; @@ -32585,8 +32605,8 @@ export interface operations { untilDate?: number; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'loginBonus' | 'pollVote' | 'groupInvited')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'loginBonus' | 'pollVote' | 'groupInvited')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'pointReceived' | 'app' | 'test' | 'loginBonus' | 'pollVote' | 'groupInvited')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'pointReceived' | 'app' | 'test' | 'loginBonus' | 'pollVote' | 'groupInvited')[]; }; }; }; @@ -33925,6 +33945,15 @@ export interface operations { /** Format: misskey:id */ userListId: string; }; + pointReceived?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; + } | { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }; app?: { /** @enum {string} */ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index aa64c208a3d..84aa38feb30 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -37,6 +37,7 @@ export const notificationTypes = [ 'test', 'login', 'createToken', + 'pointReceived', ] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts index 6dfea12aa8b..1ab22821398 100644 --- a/packages/sw/src/scripts/create-notification.ts +++ b/packages/sw/src/scripts/create-notification.ts @@ -52,6 +52,16 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif */ case 'notification': switch (data.body.type) { + case 'pointReceived': + return [i18n.tsx._notification.pointReceivedFrom({ + name: getUserName(data.body.user), + points: data.body.points, + pointName: i18n.ts.point, + }), { + icon: data.body.user.avatarUrl ?? undefined, + data, + }]; + case 'follow': { // users/showの型定義をswos.apiへ当てはめるのが困難なのでapiFetch.requestを直接使用 const account = await getAccountFromId(data.userId); From 67c2400353d1f5b1e5fe6469d44799c83643b95d Mon Sep 17 00:00:00 2001 From: mattyatea Date: Tue, 6 Oct 2026 17:57:12 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=E3=83=9D=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=83=88=E5=8F=97=E5=8F=96=E9=80=9A=E7=9F=A5=E3=81=AEUI?= =?UTF-8?q?=E3=82=B9=E3=82=AF=E3=83=AA=E3=83=BC=E3=83=B3=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=83=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../point-received-notification-dark.png | Bin 0 -> 10624 bytes .../point-received-notification-light.png | Bin 0 -> 10289 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/screenshots/point-received-notification-dark.png create mode 100644 docs/screenshots/point-received-notification-light.png diff --git a/docs/screenshots/point-received-notification-dark.png b/docs/screenshots/point-received-notification-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1b934bfb43026ce3c94b83a653c0b9cf162d58a1 GIT binary patch literal 10624 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%0D4>*A7n{ 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/point-received-notification-light.png b/docs/screenshots/point-received-notification-light.png new file mode 100644 index 0000000000000000000000000000000000000000..cfb68a8bb787c0defe399f522a3edf63de016c14 GIT binary patch literal 10289 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 TAu0ph6@Ze0ntavUk6-@>Ql!Yq literal 0 HcmV?d00001