Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2578e65
Add the `pinNotification` action.
nfmohit Aug 29, 2025
5940683
Add the `unpinNotification` action.
nfmohit Aug 29, 2025
d48efd9
Add the `getPinnedNotificationID` selector.
nfmohit Aug 29, 2025
4a2ce48
Update `POPULATE_QUEUE` control to queue a pinned notification first.
nfmohit Aug 29, 2025
3b3fd3b
Update reducer for `INSERT_NOTIFICATION_INTO_RESOLVED_QUEUE` to inser…
nfmohit Aug 29, 2025
f80fe96
Unpin a notification upon dismissal.
nfmohit Aug 29, 2025
1c43d95
Merge branch 'develop' into fix/#10890-pin-notification.
nfmohit Aug 29, 2025
ce35522
Merge branch 'develop' into fix/#10890-pin-notification.
nfmohit Sep 2, 2025
b442648
Snapshot notifications store.
nfmohit Sep 2, 2025
ac064fd
Add `onOAuthNavigation` callback to `useEnableAudienceGroup` hook.
nfmohit Sep 2, 2025
3c54b2a
Pin AS Setup notification before navigating to OAuth flow.
nfmohit Sep 2, 2025
5a93828
Pin EM setup notification before navigating to OAuth flow.
nfmohit Sep 2, 2025
f75eb33
Unpin EM setup notification on setup success.
nfmohit Sep 2, 2025
7e71810
Add missing dependency.
nfmohit Sep 2, 2025
0bc540b
Require `groupID` in `getPinnedNotificationID` selector.
nfmohit Sep 2, 2025
f420d7c
Add test coverage for `pinNotification`.
nfmohit Sep 2, 2025
e0fb095
Add test coverage for `unpinNotification`.
nfmohit Sep 2, 2025
7ec077d
Add test coverage for `getPinnedNotificationID`.
nfmohit Sep 2, 2025
880a96f
Merge branch 'develop' into fix/#10890-pin-notification.
nfmohit Oct 8, 2025
5763998
Revert "Snapshot notifications store."
nfmohit Oct 8, 2025
f5b1783
Persist pinned notifications in browser storage.
nfmohit Oct 8, 2025
befe921
Revert custom priority for multi-step notifications.
nfmohit Oct 8, 2025
a2f9607
Address minor CR feedback.
nfmohit Oct 8, 2025
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
224 changes: 212 additions & 12 deletions assets/js/googlesitekit/notifications/datastore/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,15 @@ const DISMISS_NOTIFICATION = 'DISMISS_NOTIFICATION';
const QUEUE_NOTIFICATION = 'QUEUE_NOTIFICATION';
const RESET_QUEUE = 'RESET_QUEUE';
const MARK_NOTIFICATION_SEEN = 'MARK_NOTIFICATION_SEEN';
const PIN_NOTIFICATION = 'PIN_NOTIFICATION';
const UNPIN_NOTIFICATION = 'UNPIN_NOTIFICATION';
// Controls.
const POPULATE_QUEUE = 'POPULATE_QUEUE';
const PERSIST_SEEN_NOTIFICATIONS = 'PERSIST_SEEN_NOTIFICATIONS';
const PERSIST_PINNED_NOTIFICATIONS = 'PERSIST_PINNED_NOTIFICATIONS';

const NOTIFICATION_SEEN_STORAGE_KEY = 'googlesitekit_notification_seen';
const NOTIFICATION_PINNED_STORAGE_KEY = 'googlesitekit_notification_pinned';

const storage = getStorage();

Expand All @@ -67,6 +71,9 @@ export const initialState = {
seenNotifications: JSON.parse(
storage.getItem( NOTIFICATION_SEEN_STORAGE_KEY ) || '{}'
),
pinnedNotification: JSON.parse(
storage.getItem( NOTIFICATION_PINNED_STORAGE_KEY ) || '{}'
),
};

export const actions = {
Expand Down Expand Up @@ -416,6 +423,15 @@ export const actions = {
.select( CORE_NOTIFICATIONS )
.getNotification( id );

// Check if the notification is pinned; if so, unpin it.
const pinnedNotificationID = registry
.select( CORE_NOTIFICATIONS )
.getPinnedNotificationID( notification?.groupID );

if ( pinnedNotificationID === id ) {
yield actions.unpinNotification( id, notification.groupID );
}

// Skip persisting notification dismissal in database if the notification is not dismissible.
if ( notification.isDismissible !== true ) {
return null;
Expand Down Expand Up @@ -447,6 +463,72 @@ export const actions = {
);
}
),

/**
* Pins a notification to the top of its respective queue.
*
* @since n.e.x.t
*
* @param {string} id Notification ID to pin.
* @param {string} groupID Group ID the notification belongs to.
* @return {Object} Redux-style action.
*/
pinNotification: createValidatedAction(
( id, groupID ) => {
invariant(
id,
'A notification id is required to pin a notification.'
);

invariant(
groupID,
'A groupID is required to pin a notification to a specific group.'
);
},
function* ( id, groupID ) {
yield {
type: PIN_NOTIFICATION,
payload: { id, groupID },
};

yield {
type: PERSIST_PINNED_NOTIFICATIONS,
};
}
),

/**
* Unpins a notification from the top of its respective queue.
*
* @since n.e.x.t
*
* @param {string} id Notification ID to unpin.
* @param {string} groupID Group ID the notification belongs to.
* @return {Object} Redux-style action.
*/
unpinNotification: createValidatedAction(
( id, groupID ) => {
invariant(
id,
'A notification id is required to unpin a notification.'
);

invariant(
groupID,
'A groupID is required to unpin notification from a specific group.'
);
},
function* ( id, groupID ) {
yield {
type: UNPIN_NOTIFICATION,
payload: { id, groupID },
};

yield {
type: PERSIST_PINNED_NOTIFICATIONS,
};
}
),
};

export const controls = {
Expand Down Expand Up @@ -509,6 +591,39 @@ export const controls = {
const { queueNotification } =
registry.dispatch( CORE_NOTIFICATIONS );

// Get the pinned notification ID for this group, if any.
const pinnedNotificationID = registry
.select( CORE_NOTIFICATIONS )
.getPinnedNotificationID( groupID );

if ( pinnedNotificationID ) {
// Check if a pinned notification exists within the potential notifications.
const potentialPinnedNotification =
potentialNotifications.find(
( notification ) =>
notification.id === pinnedNotificationID
);

// If the pinned notification exists within the potential notifications,
// check its requirements and add it to the queue first if they pass.
if ( potentialPinnedNotification ) {
const meetsRequirements =
await potentialPinnedNotification.check();

if ( meetsRequirements ) {
queueNotification( potentialPinnedNotification );

// Remove the pinned notification from the potential notifications.
potentialNotifications =
potentialNotifications.filter(
( notification ) =>
notification.id !==
potentialPinnedNotification.id
);
}
}
}

let nextNotification;
do {
nextNotification = await racePrioritizedAsyncTasks(
Expand All @@ -535,18 +650,32 @@ export const controls = {
);
}
),
[ PERSIST_PINNED_NOTIFICATIONS ]: createRegistryControl(
( registry ) => () => {
const pinnedNotifications = registry
.select( CORE_NOTIFICATIONS )
.getPinnedNotificationIDs();

storage.setItem(
NOTIFICATION_PINNED_STORAGE_KEY,
JSON.stringify( pinnedNotifications )
);
}
),
};

// eslint-disable-next-line complexity
export const reducer = createReducer( ( state, { type, payload } ) => {
switch ( type ) {
case INSERT_NOTIFICATION_INTO_RESOLVED_QUEUE: {
const { id } = payload;
const { notifications, pinnedNotification, queuedNotifications } =
state;

/**
* The notification we want to add to the already-resolved queue.
*/
const notification = state.notifications?.[ id ];
const notification = notifications?.[ id ];

// Don't try to add a notification that is not registered.
if ( notification === undefined ) {
Expand All @@ -557,22 +686,48 @@ export const reducer = createReducer( ( state, { type, payload } ) => {
break;
}

const { groupID, priority } = notification;

// If the queue hasn't resolved yet, the queued notifications for this
// group will be undefined. In this case we return early because this
// notification will be added to the queue once it resolves.
if (
state.queuedNotifications[ notification.groupID ] === undefined
) {
if ( queuedNotifications[ groupID ] === undefined ) {
break;
}

// If the notification is already in the queue, we don't need to add it
// again.
// Check if the notification is already in the queue.
if (
state.queuedNotifications[ notification.groupID ].some(
queuedNotifications[ groupID ].some(
( notificationInQueue ) => notificationInQueue.id === id
)
) {
// Check if it is pinned.
if ( pinnedNotification[ groupID ] === id ) {
const existingIndex = queuedNotifications[
groupID
].findIndex(
( notificationInQueue ) => notificationInQueue.id === id
);

// Remove it from its current position.
const [ existingNotification ] = queuedNotifications[
groupID
].splice( existingIndex, 1 );

// Add it to the front of the queue.
queuedNotifications[ groupID ].unshift(
existingNotification
);
}

// We don't need to add it again.
break;
}

// Check if the notification is pinned.
if ( pinnedNotification[ groupID ] === id ) {
// If it is, we add it to the front of the queue.
queuedNotifications[ groupID ].unshift( notification );
break;
}

Expand All @@ -587,18 +742,18 @@ export const reducer = createReducer( ( state, { type, payload } ) => {
// If we do find a notification with a lower priority (or the same
// priority), `findIndex` will return its index, which we can use to
// insert the new notification after that notification.
const positionForNewNotification = state.queuedNotifications[
notification.groupID
const positionForNewNotification = queuedNotifications[
groupID
].findIndex( ( notificationInQueue ) => {
return notificationInQueue.priority >= notification.priority;
return notificationInQueue.priority >= priority;
} );

// Insert the new notification at the position we found, or at the end of
// the queue if we didn't find any notification with a lower priority.
state.queuedNotifications[ notification.groupID ].splice(
queuedNotifications[ groupID ].splice(
positionForNewNotification !== -1
? positionForNewNotification
: state.queuedNotifications[ notification.groupID ].length,
: state.queuedNotifications[ groupID ].length,
0,
notification
);
Expand Down Expand Up @@ -688,6 +843,24 @@ export const reducer = createReducer( ( state, { type, payload } ) => {
break;
}

case PIN_NOTIFICATION: {
const { id, groupID } = payload;

state.pinnedNotification[ groupID ] = id;

break;
}

case UNPIN_NOTIFICATION: {
const { id, groupID } = payload;

if ( state.pinnedNotification[ groupID ] === id ) {
delete state.pinnedNotification[ groupID ];
}

break;
}

default:
break;
}
Expand Down Expand Up @@ -843,6 +1016,33 @@ export const selectors = {
return false;
}
),

/**
* Gets all pinned notification IDs, keyed by group ID.
*
* @since n.e.x.t
*
* @param {Object} state Data store's state.
* @return {Object} Object with group IDs as keys and pinned notification ID as the value.
*/
getPinnedNotificationIDs: ( state ) => {
return state.pinnedNotification;
},

/**
* Gets the ID of the pinned notification for a specific group, if any.
*
* @since n.e.x.t
*
* @param {Object} state Data store's state.
* @param {string} groupID The group ID to get the pinned notification ID for.
* @return {(string|undefined)} The ID of the pinned notification, or undefined if none is pinned.
*/
getPinnedNotificationID: ( state, groupID ) => {
invariant( groupID, 'groupID is required.' );

return state.pinnedNotification[ groupID ];
},
};

export default {
Expand Down
Loading
Loading