Skip to content

Commit

Permalink
feat(ui): Notification Management (clicking on Notification) (#572)
Browse files Browse the repository at this point in the history
* feat(ui): update redux store

* feat(ui): create notification option modal

* feat(ui): implement earlier notification

* feat(ui): add UT for notification detail and components

* feat(ui): refactor notification item

* fix(ui): fix icon of notification item

* fix(ui): reset expand when nav to another page

* fix(ui): fix review comment

* fix(ui): fix style on android

* fix(ui): fix style on android

* fix(ui): fix unit test

---------

Co-authored-by: Vu Van Duc <vuvanduc@Vus-MacBook-Pro.local>
  • Loading branch information
Sotatek-DukeVu and Vu Van Duc committed Jul 15, 2024
1 parent bbfc5ac commit bf20971
Show file tree
Hide file tree
Showing 22 changed files with 1,492 additions and 142 deletions.
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^12.2.0",
"react-infinite-scroll-component": "^6.1.0",
"react-qrcode-logo": "^2.9.0",
"react-redux": "^8.0.5",
"react-router-dom": "^5.3.4",
Expand Down
27 changes: 23 additions & 4 deletions src/locales/en/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -930,12 +930,31 @@
},
"sections": {
"new": "New",
"earlier": "Earlier"
"earlier": {
"title": "Earlier",
"end": "End of notifications",
"buttons": {
"showealier": "View previous notifications"
}
}
},
"optionmodal": {
"title": "Notification options",
"done": "Done",
"showdetail": "Show details",
"markasread": "Mark as read",
"markasunread": "Mark as unread",
"delete": "Delete notification",
"deletealert": {
"text": "Deleting this notification cannot be undone. Once deleted you will no longer be able to require any actions required from this notification.",
"accept": "Delete",
"cancel": "Cancel"
}
},
"labels": {
"exnipexgrant": "{{connection}} wants to issue you a credential",
"multisigicp": "{{connection}} is requesting to create a multi-sig identifier with you",
"exnipexapply": "{{connection}} has requested a credential from you"
"exnipexgrant": "<strong>{{connection}}</strong> wants to issue you a credential",
"multisigicp": "<strong>{{connection}}</strong> is requesting to create a multi-sig identifier with you",
"exnipexapply": "<strong>{{connection}}</strong> has requested a credential from you"
}
},
"details": {
Expand Down
55 changes: 55 additions & 0 deletions src/store/reducers/notificationsCache/notificationsCache.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import { PayloadAction } from "@reduxjs/toolkit";
import {
deleteNotification,
getNotificationsCache,
notificationsCacheSlice,
setNotificationsCache,
setReadedNotification,
} from "./notificationsCache";
import { RootState } from "../../index";
import { KeriaNotification } from "../../../core/agent/agent.types";
import { OperationType } from "../../../ui/globals/types";

const notification = {
id: "AL3XmFY8BM9F604qmV-l9b0YMZNvshHG7X6CveMWKMmG",
createdAt: "2024-06-25T12:38:36.988Z",
a: {
r: "/exn/ipex/grant",
d: "EMT02ZHUhpnr4gFFk104B-pLwb2bJC8aip2VYmbPztnk",
m: "",
},
connectionId: "EMrT7qX0FIMenQoe5pJLahxz_rheks1uIviGW8ch8pfB",
read: true,
};

describe("Notifications cache", () => {
const initialState = {
notifications: [],
Expand All @@ -18,6 +32,47 @@ describe("Notifications cache", () => {
).toEqual(initialState);
});

it("should handle setReadedNotification", () => {
const initialState = {
notifications: [notification],
};

const newState = notificationsCacheSlice.reducer(
initialState,
setReadedNotification({
id: "AL3XmFY8BM9F604qmV-l9b0YMZNvshHG7X6CveMWKMmG",
read: false,
})
);

expect(newState.notifications).toEqual([
{
id: "AL3XmFY8BM9F604qmV-l9b0YMZNvshHG7X6CveMWKMmG",
createdAt: "2024-06-25T12:38:36.988Z",
a: {
r: "/exn/ipex/grant",
d: "EMT02ZHUhpnr4gFFk104B-pLwb2bJC8aip2VYmbPztnk",
m: "",
},
connectionId: "EMrT7qX0FIMenQoe5pJLahxz_rheks1uIviGW8ch8pfB",
read: false,
},
]);
});

it("should handle deleteNotification", () => {
const initialState = {
notifications: [notification],
};

const newState = notificationsCacheSlice.reducer(
initialState,
deleteNotification(notification)
);

expect(newState.notifications).toEqual([]);
});

it("should handle setNotificationsCache", () => {
const notifications: KeriaNotification[] = [
{
Expand Down
27 changes: 26 additions & 1 deletion src/store/reducers/notificationsCache/notificationsCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@ const notificationsCacheSlice = createSlice({
name: "notificationsCache",
initialState,
reducers: {
setReadedNotification: (
state,
action: PayloadAction<{
id: string;
read: boolean;
}>
) => {
state.notifications = state.notifications.map((notification) => {
if (notification.id !== action.payload.id) return notification;

return {
...notification,
read: action.payload.read,
};
});
},
deleteNotification: (state, action: PayloadAction<KeriaNotification>) => {
state.notifications = state.notifications.filter(
(notification) => notification.id !== action.payload.id
);
},
setNotificationsCache: (
state,
action: PayloadAction<KeriaNotification[]>
Expand All @@ -22,7 +43,11 @@ const notificationsCacheSlice = createSlice({

export { initialState, notificationsCacheSlice };

export const { setNotificationsCache } = notificationsCacheSlice.actions;
export const {
setNotificationsCache,
setReadedNotification,
deleteNotification,
} = notificationsCacheSlice.actions;

const getNotificationsCache = (state: RootState) =>
state.notificationsCache.notifications;
Expand Down
3 changes: 2 additions & 1 deletion src/ui/components/layout/TabLayout/TabLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "@ionic/react";
import { arrowBackOutline } from "ionicons/icons";
import "./TabLayout.scss";
import { useCallback, useRef, useState } from "react";
import { useCallback, useState } from "react";
import { TabLayoutProps } from "./TabLayout.types";
import { useIonHardwareBackButton } from "../../../hooks";
import { BackEventPriorityType } from "../../../globals/types";
Expand Down Expand Up @@ -125,6 +125,7 @@ const TabLayout = ({
)}
{placeholder || (
<IonContent
id={pageId}
className="tab-content"
color="transparent"
>
Expand Down
105 changes: 105 additions & 0 deletions src/ui/pages/NotificationDetails/NotificationDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { IonReactMemoryRouter } from "@ionic/react-router";
import { mockIonicReact } from "@ionic/react-test-utils";
import { render, waitFor } from "@testing-library/react";
import { createMemoryHistory } from "history";
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
import EN_TRANSLATIONS from "../../../locales/en/en.json";
import { RoutePath, TabsRoutePath } from "../../../routes/paths";
import { connectionsFix } from "../../__fixtures__/connectionsFix";
import { filteredIdentifierFix } from "../../__fixtures__/filteredIdentifierFix";
import { notificationsFix } from "../../__fixtures__/notificationsFix";
import { NotificationDetails } from "./NotificationDetails";

mockIonicReact();

const getMultiSignMock = jest.fn().mockResolvedValue({
sender: {
label: "CF Credential Issuance",
},
otherConnections: connectionsFix,
});

jest.mock("../../../core/agent/agent", () => ({
Agent: {
agent: {
basicStorage: {
deleteById: jest.fn(() => Promise.resolve()),
},
multiSigs: {
getMultisigIcpDetails: () => getMultiSignMock(),
},
},
},
}));

const mockStore = configureStore();
const dispatchMock = jest.fn();
const initialState = {
stateCache: {
routes: [TabsRoutePath.NOTIFICATIONS],
authentication: {
loggedIn: true,
time: Date.now(),
passcodeIsSet: true,
},
},
connectionsCache: {
connections: [],
},
identifiersCache: {
identifiers: filteredIdentifierFix,
},
notificationsCache: {
notifications: notificationsFix,
},
};

describe("Notification Detail", () => {
test("render credential receiver request", async () => {
const storeMocked = {
...mockStore(initialState),
dispatch: dispatchMock,
};

const history = createMemoryHistory();
history.push(RoutePath.CONNECTION_DETAILS, notificationsFix[0]);

const { getByText } = render(
<IonReactMemoryRouter history={history}>
<Provider store={storeMocked}>
<NotificationDetails />
</Provider>
</IonReactMemoryRouter>
);
expect(
getByText(EN_TRANSLATIONS.notifications.details.credential.receive.title)
).toBeVisible();
expect(
getByText(EN_TRANSLATIONS.notifications.details.buttons.close)
).toBeVisible();
});

test("render mutil-sign request", async () => {
const storeMocked = {
...mockStore(initialState),
dispatch: dispatchMock,
};

const history = createMemoryHistory();
history.push(RoutePath.CONNECTION_DETAILS, notificationsFix[3]);

const { getByText } = render(
<IonReactMemoryRouter history={history}>
<Provider store={storeMocked}>
<NotificationDetails />
</Provider>
</IonReactMemoryRouter>
);
await waitFor(() => {
expect(
getByText(EN_TRANSLATIONS.notifications.details.identifier.title)
).toBeVisible();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
margin-bottom: 1.5rem;

ion-list {
&.md {
padding: 0;
}

ion-grid {
padding: 0;

Expand Down
Loading

0 comments on commit bf20971

Please sign in to comment.