Skip to content

Commit

Permalink
AnnouncementController migration to BaseControllerV2 (#959)
Browse files Browse the repository at this point in the history
AnnouncementController migration to BaseControllerV2

Resolves #945
  • Loading branch information
vthomas13 authored and MajorLift committed Oct 11, 2023
1 parent 48ded0a commit 9a73e28
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 82 deletions.
1 change: 1 addition & 0 deletions packages/announcement-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@metamask/auto-changelog": "^3.1.0",
"@types/jest": "^26.0.22",
"deepmerge": "^4.2.2",
"immer": "^9.0.6",
"jest": "^26.4.2",
"ts-jest": "^26.5.2",
"typedoc": "^0.22.15",
Expand Down
104 changes: 76 additions & 28 deletions packages/announcement-controller/src/AnnouncementController.test.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,102 @@
import { ControllerMessenger } from '@metamask/base-controller';
import {
AnnouncementConfig,
AnnouncementState,
AnnouncementControllerState,
AnnouncementController,
StateAnnouncementMap,
AnnouncementControllerActions,
AnnouncementControllerEvents,
AnnouncementMap,
} from './AnnouncementController';

const config1: AnnouncementConfig = {
allAnnouncements: {
const name = 'AnnouncementController';

/**
* Constructs a restricted controller messenger.
*
* @returns A restricted controller messenger.
*/
function getRestrictedMessenger() {
const controllerMessenger = new ControllerMessenger<
AnnouncementControllerActions,
AnnouncementControllerEvents
>();
return controllerMessenger.getRestricted<typeof name, never, never>({
name,
});
}
const allAnnouncements: AnnouncementMap = {
1: {
id: 1,
date: '12/8/2020',
},
2: {
id: 2,
date: '12/8/2020',
},
};
const allAnnouncements2: AnnouncementMap = {
1: {
id: 1,
date: '12/8/2020',
},
2: {
id: 2,
date: '12/8/2020',
},
3: {
id: 3,
date: '12/8/2020',
},
};
const state1: AnnouncementControllerState = {
announcements: {
1: {
id: 1,
date: '12/8/2020',
isShown: true,
},
2: {
id: 2,
date: '12/8/2020',
isShown: true,
},
},
};

const config2: AnnouncementConfig = {
allAnnouncements: {
const state2: AnnouncementControllerState = {
announcements: {
1: {
id: 1,
date: '12/8/2020',
isShown: false,
},
2: {
id: 2,
date: '12/8/2020',
isShown: false,
},
3: {
id: 3,
date: '12/8/2020',
},
},
};

const state1: AnnouncementState = {
announcements: {
1: {
id: 1,
date: '12/8/2020',
isShown: true,
},
2: {
id: 2,
date: '12/8/2020',
isShown: true,
isShown: false,
},
},
};

describe('announcement controller', () => {
it('should add announcement to state', () => {
const controller = new AnnouncementController(config1);
const controller = new AnnouncementController({
messenger: getRestrictedMessenger(),
allAnnouncements,
});
expect(Object.keys(controller.state.announcements)).toHaveLength(2);
const expectedStateNotifications: StateAnnouncementMap = {
1: {
...config1.allAnnouncements[1],
...allAnnouncements[1],
isShown: false,
},
2: {
...config1.allAnnouncements[2],
...allAnnouncements[2],
isShown: false,
},
};
Expand All @@ -69,8 +105,12 @@ describe('announcement controller', () => {
);
});

it('should add new announcement to state', () => {
const controller = new AnnouncementController(config2, state1);
it('should add new announcement to state and a new announcement should be created with isShown as false', () => {
const controller = new AnnouncementController({
messenger: getRestrictedMessenger(),
state: state1,
allAnnouncements: allAnnouncements2,
});
expect(Object.keys(controller.state.announcements)).toHaveLength(3);
expect(controller.state.announcements[1].isShown).toBe(true);
expect(controller.state.announcements[2].isShown).toBe(true);
Expand All @@ -79,15 +119,23 @@ describe('announcement controller', () => {

describe('update viewed announcements', () => {
it('should update isShown status', () => {
const controller = new AnnouncementController(config2);
const controller = new AnnouncementController({
messenger: getRestrictedMessenger(),
state: state2,
allAnnouncements: allAnnouncements2,
});
controller.updateViewed({ 1: true });
expect(controller.state.announcements[1].isShown).toBe(true);
expect(controller.state.announcements[2].isShown).toBe(false);
expect(controller.state.announcements[3].isShown).toBe(false);
});

it('should update isShown of more than one announcement', () => {
const controller = new AnnouncementController(config2);
const controller = new AnnouncementController({
messenger: getRestrictedMessenger(),
state: state2,
allAnnouncements: allAnnouncements2,
});
controller.updateViewed({ 2: true, 3: true });
expect(controller.state.announcements[1].isShown).toBe(false);
expect(controller.state.announcements[2].isShown).toBe(true);
Expand Down
135 changes: 81 additions & 54 deletions packages/announcement-controller/src/AnnouncementController.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,124 @@
import type { Patch } from 'immer';
import {
BaseController,
BaseConfig,
BaseState,
BaseControllerV2,
RestrictedControllerMessenger,
} from '@metamask/base-controller';

interface ViewedAnnouncement {
type ViewedAnnouncement = {
[id: number]: boolean;
}
};

interface Announcement {
type Announcement = {
id: number;
date: string;
}

interface StateAnnouncement extends Announcement {
isShown: boolean;
}
};

/**
* A map of announcement ids to Announcement objects
*/
interface AnnouncementMap {
export type AnnouncementMap = {
[id: number]: Announcement;
}
};

type StateAnnouncement = Announcement & { isShown: boolean };

/**
* A map of announcement ids to StateAnnouncement objects
*/
export interface StateAnnouncementMap {
export type StateAnnouncementMap = {
[id: number]: StateAnnouncement;
}

/**
* AnnouncementConfig will hold the active announcements
*/
export interface AnnouncementConfig extends BaseConfig {
allAnnouncements: AnnouncementMap;
}
};

/**
* Announcement state will hold all the seen and unseen announcements
* that are still active
*/
export interface AnnouncementState extends BaseState {
export type AnnouncementControllerState = {
announcements: StateAnnouncementMap;
}
};

export type AnnouncementControllerActions =
AnnouncementControllerGetStateAction;
export type AnnouncementControllerEvents =
AnnouncementControllerStateChangeEvent;

export type AnnouncementControllerGetStateAction = {
type: `${typeof controllerName}:getState`;
handler: () => AnnouncementControllerState;
};

export type AnnouncementControllerStateChangeEvent = {
type: `${typeof controllerName}:stateChange`;
payload: [AnnouncementControllerState, Patch[]];
};

const controllerName = 'AnnouncementController';

const defaultState = {
announcements: {},
};

const metadata = {
announcements: {
persist: true,
anonymous: true,
},
};

export type AnnouncementControllerMessenger = RestrictedControllerMessenger<
typeof controllerName,
AnnouncementControllerActions,
AnnouncementControllerEvents,
never,
never
>;

/**
* Controller for managing in-app announcements.
*/
export class AnnouncementController extends BaseController<
AnnouncementConfig,
AnnouncementState
export class AnnouncementController extends BaseControllerV2<
typeof controllerName,
AnnouncementControllerState,
AnnouncementControllerMessenger
> {
/**
* Creates a AnnouncementController instance.
*
* @param config - Initial options used to configure this controller.
* @param state - Initial state to set on this controller.
* @param args - The arguments to this function.
* @param args.messenger - Messenger used to communicate with BaseV2 controller.
* @param args.state - Initial state to set on this controller.
* @param args.allAnnouncements - Announcements to be passed through to #addAnnouncements
*/
constructor(config: AnnouncementConfig, state?: AnnouncementState) {
super(config, state || defaultState);
this.initialize();
this._addAnnouncements();
constructor({
messenger,
state,
allAnnouncements,
}: {
messenger: AnnouncementControllerMessenger;
state?: AnnouncementControllerState;
allAnnouncements: AnnouncementMap;
}) {
const mergedState = { ...defaultState, ...state };
super({ messenger, metadata, name: controllerName, state: mergedState });
this.#addAnnouncements(allAnnouncements);
}

/**
* Compares the announcements in state with the announcements from file
* to check if there are any new announcements
* if yes, the new announcement will be added to the state with a flag indicating
* that the announcement is not seen by the user.
*
* @param allAnnouncements - all announcements to compare with the announcements from state
*/
private _addAnnouncements(): void {
const newAnnouncements: StateAnnouncementMap = {};
const { allAnnouncements } = this.config;
Object.values(allAnnouncements).forEach(
(announcement: StateAnnouncement) => {
newAnnouncements[announcement.id] = this.state.announcements[
#addAnnouncements(allAnnouncements: AnnouncementMap): void {
this.update((state) => {
Object.values(allAnnouncements).forEach((announcement: Announcement) => {
state.announcements[announcement.id] = state.announcements[
announcement.id
]
? this.state.announcements[announcement.id]
: {
...announcement,
isShown: false,
};
},
);
this.update({ announcements: newAnnouncements });
] ?? { ...announcement, isShown: false };
});
});
}

/**
Expand All @@ -100,11 +128,10 @@ export class AnnouncementController extends BaseController<
* @param viewedIds - The announcement IDs to mark as viewed.
*/
updateViewed(viewedIds: ViewedAnnouncement): void {
const stateAnnouncements = this.state.announcements;

for (const id of Object.keys(viewedIds).map(Number)) {
stateAnnouncements[id].isShown = viewedIds[id];
}
this.update({ announcements: stateAnnouncements }, true);
this.update(({ announcements }) => {
for (const id of Object.keys(viewedIds).map(Number)) {
announcements[id].isShown = viewedIds[id];
}
});
}
}
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,7 @@ __metadata:
"@metamask/base-controller": "workspace:^"
"@types/jest": ^26.0.22
deepmerge: ^4.2.2
immer: ^9.0.6
jest: ^26.4.2
ts-jest: ^26.5.2
typedoc: ^0.22.15
Expand Down

0 comments on commit 9a73e28

Please sign in to comment.