Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/batch-open-requests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@trycourier/courier-js": patch
"@trycourier/courier-ui-inbox": patch
"@trycourier/courier-react-components": patch
---

Batch open requests to reduce network overhead. Multiple messages becoming visible within a short window are now collected and sent to the server in a single GraphQL mutation instead of individual requests per message.
6 changes: 6 additions & 0 deletions @trycourier/courier-js/src/__tests__/inbox-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ describeIntegration('InboxClient', () => {
})).resolves.not.toThrow();
});

itWithMessageEnv('should batch open multiple messages', async () => {
await expect(courierClient.inbox.batchOpen([
process.env.MESSAGE_ID!,
])).resolves.not.toThrow();
});

itWithMessageEnv('should archive message', async () => {
await expect(courierClient.inbox.archive({
messageId: process.env.MESSAGE_ID!
Expand Down
39 changes: 39 additions & 0 deletions @trycourier/courier-js/src/client/inbox-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,45 @@ export class InboxClient extends Client {
});
}

/**
* Mark multiple messages as opened in a single request.
* @param messageIds - IDs of the messages to mark as opened
* @returns Promise resolving when all messages are marked as opened
*/
public async batchOpen(messageIds: string[]): Promise<void> {
if (messageIds.length === 0) return;

if (messageIds.length === 1) {
return this.open({ messageId: messageIds[0] });
}

const mutations = messageIds.map((id, index) =>
`open_${index}: opened(messageId: "${id}")`
);

const query = `
mutation BatchTrackEvents {
${mutations.join('\n ')}
}
`;

const headers: Record<string, string> = {
'x-courier-user-id': this.options.userId,
'Authorization': `Bearer ${this.options.accessToken}`
};

if (this.options.connectionId) {
headers['x-courier-client-source-id'] = this.options.connectionId;
}

await graphql({
options: this.options,
query,
headers,
url: this.options.apiUrls.inbox.graphql,
});
}

/**
* Archive a message
* @param messageId - ID of the message to archive
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useEffect, forwardRef, ReactNode, useContext, useState } from "react";
import { useRef, useEffect, useMemo, forwardRef, ReactNode, useContext, useState } from "react";
import { CourierInboxListItemActionFactoryProps, CourierInboxListItemFactoryProps, CourierInboxTheme, CourierInbox as CourierInboxElement, CourierInboxHeaderFactoryProps, CourierInboxStateEmptyFactoryProps, CourierInboxStateLoadingFactoryProps, CourierInboxStateErrorFactoryProps, CourierInboxPaginationItemFactoryProps, CourierInboxFeed } from "@trycourier/courier-ui-inbox";
import { CourierComponentThemeMode } from "@trycourier/courier-ui-core";
import { CourierClientComponent } from "./courier-client-component";
Expand Down Expand Up @@ -60,7 +60,6 @@ export const CourierInboxComponent = forwardRef<CourierInboxElement, CourierInbo
// Element ref for use in effects, updated by handleRef.
const inboxRef = useRef<CourierInboxElement | null>(null);
const [elementReady, setElementReady] = useState(false);
const feedsSetRef = useRef(false);

// Callback ref passed to rendered component, used to propagate the DOM element's ref to the parent component.
// We use a callback ref (rather than a React.RefObject) since we want the parent ref to be up-to-date with
Expand Down Expand Up @@ -184,15 +183,10 @@ export const CourierInboxComponent = forwardRef<CourierInboxElement, CourierInbo
});
}, [props.renderPaginationItem, elementReady]);

// Set feeds (only once when element is ready)
useEffect(() => {
const inbox = getEl();
if (!inbox || !props.feeds || feedsSetRef.current) return;
feedsSetRef.current = true;
queueMicrotask(() => {
inbox.setFeeds(props.feeds!);
});
}, [props.feeds, elementReady]);
const feedsAttr = useMemo(
() => props.feeds ? JSON.stringify(props.feeds) : undefined,
[props.feeds]
);

const children = (
/* @ts-ignore */
Expand All @@ -202,6 +196,7 @@ export const CourierInboxComponent = forwardRef<CourierInboxElement, CourierInbo
light-theme={props.lightTheme ? JSON.stringify(props.lightTheme) : undefined}
dark-theme={props.darkTheme ? JSON.stringify(props.darkTheme) : undefined}
mode={props.mode}
feeds={feedsAttr as any}
/>
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, forwardRef, ReactNode, useContext, useState } from 'react';
import { useEffect, useMemo, useRef, forwardRef, ReactNode, useContext, useState } from 'react';
import {
CourierInboxHeaderFactoryProps,
CourierInboxListItemActionFactoryProps,
Expand Down Expand Up @@ -92,7 +92,6 @@ export const CourierInboxPopupMenuComponent = forwardRef<CourierInboxPopupMenuEl
// Element ref for use in effects, updated by handleRef.
const inboxRef = useRef<CourierInboxPopupMenuElement | null>(null);
const [elementReady, setElementReady] = useState(false);
const feedsSetRef = useRef(false);

// Callback ref passed to rendered component, used to propagate the DOM element's ref to the parent component.
// We use a callback ref (rather than a React.RefObject) since we want the parent ref to be up-to-date with
Expand Down Expand Up @@ -225,15 +224,10 @@ export const CourierInboxPopupMenuComponent = forwardRef<CourierInboxPopupMenuEl
});
}, [props.renderMenuButton, elementReady]);

// Set feeds (only once when element is ready)
useEffect(() => {
const menu = getEl();
if (!menu || !props.feeds || feedsSetRef.current) return;
feedsSetRef.current = true;
queueMicrotask(() => {
menu.setFeeds(props.feeds!);
});
}, [props.feeds, elementReady]);
const feedsAttr = useMemo(
() => props.feeds ? JSON.stringify(props.feeds) : undefined,
[props.feeds]
);

const children = (
/* @ts-ignore */
Expand All @@ -249,6 +243,7 @@ export const CourierInboxPopupMenuComponent = forwardRef<CourierInboxPopupMenuEl
light-theme={props.lightTheme ? JSON.stringify(props.lightTheme) : undefined}
dark-theme={props.darkTheme ? JSON.stringify(props.darkTheme) : undefined}
mode={props.mode}
feeds={feedsAttr as any}
/>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type InboxHooks = {
unreadMessage: (message: InboxMessage) => Promise<void>,
clickMessage: (message: InboxMessage) => Promise<void>,
archiveMessage: (message: InboxMessage) => Promise<void>,
openMessage: (message: InboxMessage) => Promise<void>,
openMessage: (message: InboxMessage) => void,
unarchiveMessage: (message: InboxMessage) => Promise<void>,
readAllMessages: () => Promise<void>,
registerFeeds: (feeds: CourierInboxFeed[]) => void,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,12 +383,8 @@ export class CourierInboxList extends CourierBaseElement {
}
}

private async openVisibleMessage(message: InboxMessage) {
try {
await openMessage(message);
} catch (error) {
// Error ignored. Will get logged in the openMessage function
}
private openVisibleMessage(message: InboxMessage) {
openMessage(message);
}

// Factories
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,14 @@ export class CourierInboxPopupMenu extends CourierBaseElement implements Courier
// State
private _totalUnreadCount: number = 0;

// Feeds (stored from attribute before inner inbox is created)
private _feeds?: CourierInboxFeed[];

// Factories
private _popupMenuButtonFactory?: (props: CourierInboxMenuButtonFactoryProps | undefined | null) => HTMLElement;

static get observedAttributes() {
return ['popup-alignment', 'message-click', 'message-action-click', 'message-long-press', 'popup-width', 'popup-height', 'top', 'right', 'bottom', 'left', 'light-theme', 'dark-theme', 'mode'];
return ['popup-alignment', 'feeds', 'message-click', 'message-action-click', 'message-long-press', 'popup-width', 'popup-height', 'top', 'right', 'bottom', 'left', 'light-theme', 'dark-theme', 'mode'];
}

constructor() {
Expand Down Expand Up @@ -108,6 +111,9 @@ export class CourierInboxPopupMenu extends CourierBaseElement implements Courier
// Create content container
this._inbox = new CourierInbox(this._themeManager);
this._inbox.setAttribute('height', '100%');
if (this._feeds) {
this._inbox.setAttribute('feeds', JSON.stringify(this._feeds));
}

this.refreshTheme();

Expand Down Expand Up @@ -242,6 +248,19 @@ export class CourierInboxPopupMenu extends CourierBaseElement implements Courier
this._left = newValue;
this.updatePopupPosition();
break;
case 'feeds':
if (newValue) {
try {
const feeds = JSON.parse(newValue);
this._feeds = feeds;
if (this._inbox) {
this._inbox.setFeeds(feeds);
}
} catch (error) {
Courier.shared.client?.options.logger?.error('Failed to parse feeds attribute:', error);
}
}
break;
case 'light-theme':
if (newValue) {
this.setLightTheme(JSON.parse(newValue));
Expand Down
17 changes: 16 additions & 1 deletion @trycourier/courier-ui-inbox/src/components/courier-inbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export class CourierInbox extends CourierBaseElement {
};

static get observedAttributes() {
return ['height', 'light-theme', 'dark-theme', 'mode', 'message-click', 'message-action-click', 'message-long-press'];
return ['height', 'light-theme', 'dark-theme', 'mode', 'feeds', 'message-click', 'message-action-click', 'message-long-press'];
}

constructor(themeManager?: CourierInboxThemeManager) {
Expand Down Expand Up @@ -362,6 +362,7 @@ export class CourierInbox extends CourierBaseElement {
}
},
onMessageClick: (message, index) => {
CourierInboxDatastore.shared.openMessage({ message });
CourierInboxDatastore.shared.clickMessage({ message });

this.dispatchEvent(new CustomEvent('message-click', {
Expand Down Expand Up @@ -765,6 +766,20 @@ export class CourierInbox extends CourierBaseElement {
this._onMessageLongPress = undefined;
}
break;
case 'feeds':
if (newValue) {
try {
const feeds = JSON.parse(newValue);
if (this._datastoreListener) {
this.setFeeds(feeds);
} else {
this._feeds = feeds;
}
} catch (error) {
Courier.shared.client?.options.logger?.error('Failed to parse feeds attribute:', error);
}
}
break;
case 'light-theme':
if (newValue) {
this.setLightTheme(JSON.parse(newValue));
Expand Down
Loading
Loading