Skip to content
Open
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
21 changes: 19 additions & 2 deletions aselo-webchat-react-app/package-lock.json

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

5 changes: 3 additions & 2 deletions aselo-webchat-react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"scripts": {
"preinstall": "cd ../lambdas/packages/hrm-form-definitions && npm ci && npm run build && cd ../hrm-types && npm ci && npm run build",
"start": "react-app-rewired start",
"start:as_dev": "cross-env REACT_APP_CONFIG_URL='http://localhost:9090/as/development.json' run-p dev:merge-configs dev:local-config-server start",
"start:as_dev": "cross-env REACT_APP_CONFIG_URL='http://localhost:9090/as/development.json' REACT_APP_FORM_DEFINITIONS_BASE_URL='http://localhost:8091' run-p dev:merge-configs dev:local-config-server dev:local-form-definition-server start",
"build": "react-app-rewired build",
"lint": "eslint --ext js --ext jsx --ext ts --ext tsx src/",
"lint:fix": "npm run lint -- --fix",
Expand All @@ -97,7 +97,8 @@
"eject": "react-app-rewired eject",
"test:e2e": "cypress open",
"dev:merge-configs": "npm run mergeConfigs development as",
"dev:local-config-server": "npx http-server ./mergedConfigs -p 9090 --cors -c-1"
"dev:local-config-server": "npx http-server ./mergedConfigs -p 9090 --cors -c-1",
"dev:local-form-definition-server": "npx http-server ../lambdas/packages/hrm-form-definitions/form-definitions -p 8091 --cors -c-1"
},
"browserslist": [
">0.2%",
Expand Down
22 changes: 16 additions & 6 deletions aselo-webchat-react-app/src/components/MessageBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
readStatusStyles,
bubbleAndAvatarContainerStyles,
} from './styles/MessageBubble.styles';
import { selectCurrentTranslations } from '../store/config.reducer';
import { localizeKey } from '../localization/localizeKey';

const doubleDigit = (number: number) => `${number < 10 ? 0 : ''}${number}`;

Expand All @@ -56,12 +58,12 @@ export const MessageBubble = ({
}) => {
const [read, setRead] = useState(false);
const [isMouseDown, setIsMouseDown] = useState(false);
const { conversationsClient, participants, fileAttachmentConfig, participantNames } = useSelector(
const { conversationsClient, participants, fileAttachmentConfig, currentTranslations } = useSelector(
(state: AppState) => ({
conversationsClient: state.chat.conversationsClient,
participants: state.chat.participants,
fileAttachmentConfig: state.config.fileAttachment,
participantNames: state.chat.participantNames,
currentTranslations: selectCurrentTranslations(state),
}),
);
const messageRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -129,7 +131,15 @@ export const MessageBubble = ({
};

// const author = users?.find((u) => u.identity === message.author)?.friendlyName || message.author;
const name = participantNames ? participantNames[message.participantSid] : '';
let name: string;
if (belongsToCurrentUser) {
name = 'MessagePhase-MessageBubble-OwnMessageSenderName';
} else if (message.participantSid) {
name = 'MessagePhase-MessageBubble-OtherParticipantMessageSenderName';
} else {
name = message.author || '';
}
const translatedName = localizeKey(currentTranslations)(name);
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces new i18n keys (MessagePhase-MessageBubble-OwnMessageSenderName / ...OtherParticipant...) and unconditionally runs them through localizeKey(currentTranslations). Only translationsSrc/en.json defines these keys right now; es.json, fr.json, mt.json, etc. don’t, so users on those locales will see the raw key strings rendered in the UI.

Add these keys to the other locale files (even if temporarily untranslated) or implement a fallback to defaultLocale/English when a key is missing from the current locale.

Suggested change
const translatedName = localizeKey(currentTranslations)(name);
const translated = localizeKey(currentTranslations)(name);
const translatedName = translated && translated !== name ? translated : message.author || '';

Copilot uses AI. Check for mistakes.

return (
<Box
Expand All @@ -151,10 +161,10 @@ export const MessageBubble = ({
)}
<Box {...getInnerContainerStyles(belongsToCurrentUser)}>
<Flex hAlignContent="between" width="100%" vAlignContent="center" marginBottom="space20">
<Text {...authorStyles} as="p" aria-hidden style={{ textOverflow: 'ellipsis' }} title={name}>
{name}
<Text {...authorStyles} as="p" aria-hidden style={{ textOverflow: 'ellipsis' }} title={translatedName}>
{translatedName}
</Text>
<ScreenReaderOnly as="p">{belongsToCurrentUser ? 'You sent at' : `${name} sent at`}</ScreenReaderOnly>
<ScreenReaderOnly as="p">{`${translatedName} sent at`}</ScreenReaderOnly>
<Text {...timeStampStyles} as="p">
{`${doubleDigit(message.dateCreated.getHours())}:${doubleDigit(message.dateCreated.getMinutes())}`}
</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,16 @@ const user2 = {
sid: '62563cwqdc213cec23',
};
const defaultState = {
config: { fileAttachment: fileAttachmentConfig },
config: {
fileAttachment: fileAttachmentConfig,
translations: {
'en-US': {
'MessagePhase-MessageBubble-OwnMessageSenderName': 'You',
'MessagePhase-MessageBubble-OtherParticipantMessageSenderName': 'Counsellor',
},
},
defaultLocale: 'en-US',
},
chat: {
conversationsClient: { user: user1 },
participants: [{ identity: user1.identity }, { identity: user2.identity }],
Expand Down Expand Up @@ -115,7 +124,7 @@ describe('Message Bubble', () => {
/>,
);

expect(queryByText(user1.friendlyName)).toBeInTheDocument();
expect(queryByText('You')).toBeInTheDocument();
});

it('renders the message body', () => {
Expand Down Expand Up @@ -172,7 +181,7 @@ describe('Message Bubble', () => {
/>,
);

expect(getByText(`${user2.friendlyName} sent at`)).toBeInTheDocument();
expect(getByText('Counsellor sent at')).toBeInTheDocument();
});

it("renders 'is read' and icon when message read by other participant", () => {
Expand Down
5 changes: 4 additions & 1 deletion aselo-webchat-react-app/src/services/configService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export const getDefinitionVersion = async ({
}): Promise<{
preEngagementForm: PreEngagementForm;
}> => {
const formDefinitionsBaseUrl = buildFormDefinitionsBaseUrlGetter({ environment })(definitionVersionId);
const formDefinitionsBaseUrl = buildFormDefinitionsBaseUrlGetter({
environment,
configuredFormDefinitionsBaseUrl: process.env.REACT_APP_FORM_DEFINITIONS_BASE_URL,
})(definitionVersionId);
// eslint-disable-next-line
const definition = await loadWebchatDefinition(formDefinitionsBaseUrl);
return definition;
Expand Down
19 changes: 13 additions & 6 deletions aselo-webchat-react-app/src/sessionDataHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import { LocalStorageUtil } from './utils/LocalStorage';
import { generateMixPanelHeaders, generateSecurityHeaders } from './utils/generateHeaders';
import { ConfigState } from './store/definitions';
import { store } from './store/store';
import { localizeKey } from './localization/localizeKey';

export const LOCALSTORAGE_SESSION_ITEM_ID = 'TWILIO_WEBCHAT_WIDGET';
const CUSTOMER_DEFAULT_NAME = 'Customer';
const CUSTOMER_DEFAULT_NAME_KEY = 'Conversation-Participants-CustomerDefaultName';

type SessionDataStorage = TokenResponse & {
loginTimestamp: string | null;
Expand Down Expand Up @@ -189,19 +190,25 @@ class SessionDataHandler {
});

try {
const { config } = store.getState();
const payload: InitWebchatAPIPayload = {
DeploymentKey: this.getDeploymentKey(),
CustomerFriendlyName: (formData?.friendlyName as string) || CUSTOMER_DEFAULT_NAME,
CustomerFriendlyName: (() => {
const localizedName = localizeKey(config.translations[config.currentLocale ?? config.defaultLocale])(
CUSTOMER_DEFAULT_NAME_KEY,
);
return (
(formData?.friendlyName as string) ||
(localizedName === CUSTOMER_DEFAULT_NAME_KEY ? 'Anonymous' : localizedName)
);
})(),
PreEngagementData: JSON.stringify(formData),
Comment on lines +193 to 205
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CustomerFriendlyName falls back to a localized key lookup, but Conversation-Participants-CustomerDefaultName is only present in translationsSrc/en.json right now. For other locales (or for configs/tests where translations is empty), localizeKey(...) will return the raw key string, which then becomes the conversation friendly name.

Either add this key to all locale translation files (even as an English placeholder), or add a safer fallback (e.g., if the lookup returns the key unchanged, use a hardcoded default like "Anonymous").

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 9eda176. sessionDataHandler.ts now compares the localizeKey result against the raw key — if they're equal (key not found in translations), it falls back to the hardcoded 'Anonymous' string instead of passing the raw key as the friendly name.

};

if (customerIdentity) {
payload.Identity = customerIdentity;
}
newTokenData = await contactBackend(store.getState().config)<TokenResponse>(
'/webchatAuthentication/initWebchat',
payload,
);
newTokenData = await contactBackend(config)<TokenResponse>('/webchatAuthentication/initWebchat', payload);
} catch (e) {
logger.error('No results from server');
throw Error('No results from server');
Expand Down
5 changes: 4 additions & 1 deletion aselo-webchat-react-app/translationsSrc/en.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
{
"Conversation-Participants-CustomerDefaultName": "Anonymous",
"Header-CloseChatButtons-EndChatButtonLabel": "End Chat",
"Header-CloseChatButtons-EndChatConfirmDialogMessageFromChat": "End the current chat? Your messages will be lost.",
"Header-CloseChatButtons-EndChatConfirmDialogMessageFromPreEngagement": "Abandon the current chat?",
"Header-CloseChatButtons-QuickExitButtonLabel": "Quick Exit",
"Header-CloseChatButtons-QuickExitDescription": "Need to leave immediately?",
"Header-TitleBar-Title": "Live Chat",
"MessagePhase-MessageList-ChatStartMessage": "Chat Started"
"MessagePhase-MessageList-ChatStartMessage": "Chat Started",
"MessagePhase-MessageBubble-OtherParticipantMessageSenderName": "Counsellor",
"MessagePhase-MessageBubble-OwnMessageSenderName": "You"
}
39 changes: 29 additions & 10 deletions lambdas/account-scoped/src/conversation/createConversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { Twilio } from 'twilio';
import { ConversationSID } from '@tech-matters/twilio-types';
import { AseloCustomChannel } from '../customChannels/aseloCustomChannels';
import { newErr, newOk, Result } from '../Result';

const CONVERSATION_CLOSE_TIMEOUT = 'P3D'; // ISO 8601 duration format https://en.wikipedia.org/wiki/ISO_8601
export type CreateFlexConversationParams = {
Expand Down Expand Up @@ -47,26 +48,42 @@ export const createConversation = async (
additionalConversationAttributes,
testSessionId,
}: CreateFlexConversationParams,
): Promise<{ conversationSid: ConversationSID; error?: Error }> => {
): Promise<
Result<
{ conversationSid?: ConversationSID; cause: Error },
{ conversationSid: ConversationSID }
>
> => {
if (testSessionId) {
console.info(
'testSessionId specified. All outgoing messages will be sent to the test API.',
);
}

const conversationInstance = await client.conversations.v1.conversations.create({
xTwilioWebhookEnabled: 'true',
friendlyName: conversationFriendlyName,
uniqueName: `${channelType}/${uniqueUserName}/${Date.now()}`,
});
const conversationSid = conversationInstance.sid as ConversationSID;
let conversationSid: ConversationSID;
try {
const conversationInstance = await client.conversations.v1.conversations.create({
xTwilioWebhookEnabled: 'true',
friendlyName: conversationFriendlyName,
uniqueName: `${channelType}/${uniqueUserName}/${Date.now()}`,
});
conversationSid = conversationInstance.sid as ConversationSID;
} catch (err) {
return newErr({
message: `Create conversation failed: ${(err as Error)?.message}`,
error: { cause: err as Error },
});
}

try {
const conversationContext =
client.conversations.v1.conversations.get(conversationSid);
await conversationContext.participants.create({
identity: uniqueUserName,
});
await client.conversations.v1.users
.get(uniqueUserName)
.update({ friendlyName: senderScreenName });
const channelAttributes = JSON.parse((await conversationContext.fetch()).attributes);

console.debug('channelAttributes prior to update', channelAttributes);
Expand All @@ -91,7 +108,6 @@ export const createConversation = async (
'configuration.filters': ['onMessageAdded'],
});
if (onMessageAddedWebhookUrl) {
/* const onMessageAdded = */
await conversationContext.webhooks.create({
target: 'webhook',
'configuration.method': 'POST',
Expand All @@ -100,8 +116,11 @@ export const createConversation = async (
});
}
} catch (err) {
return { conversationSid, error: err as Error };
return newErr({
message: `Create conversation failed: ${(err as Error)?.message}`,
error: { conversationSid, cause: err as Error },
});
}

return { conversationSid };
return newOk({ conversationSid });
};
41 changes: 22 additions & 19 deletions lambdas/account-scoped/src/customChannels/customChannelToFlex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
createConversation,
CreateFlexConversationParams,
} from '../conversation/createConversation';
import { isErr } from '../Result';

export const findExistingConversation = async (
client: Twilio,
Expand Down Expand Up @@ -75,7 +76,7 @@ export const removeConversation = async (
}: {
conversationSid: ConversationSID;
},
) => client.conversations.v1.conversations(conversationSid).remove();
) => client.conversations.v1.conversations.get(conversationSid).remove();

export { AseloCustomChannel, isAseloCustomChannel } from './aseloCustomChannels';

Expand Down Expand Up @@ -125,26 +126,28 @@ export const sendConversationMessageToFlex = async (
let conversationSid = await findExistingConversation(client, uniqueUserName);

if (!conversationSid) {
const { conversationSid: newConversationSid, error } = await createConversation(
client,
{
studioFlowSid,
channelType,
twilioNumber,
uniqueUserName,
senderScreenName,
onMessageAddedWebhookUrl,
conversationFriendlyName,
testSessionId,
},
);
const createConversationResult = await createConversation(client, {
studioFlowSid,
channelType,
twilioNumber,
uniqueUserName,
Comment on lines 128 to +133
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the new variable name: createConverationResult is missing an "s" (should be createConversationResult). This makes the code harder to read/search and can propagate to other call sites via copy/paste.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on #4115 (comment)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 9eda176. Renamed createConverationResultcreateConversationResult throughout customChannelToFlex.ts.

senderScreenName,
onMessageAddedWebhookUrl,
conversationFriendlyName,
testSessionId,
});

if (error) {
await removeConversation(client, {
conversationSid: newConversationSid,
});
throw error;
if (isErr(createConversationResult)) {
const { conversationSid: newConversationSid, cause } =
createConversationResult.error;
if (newConversationSid) {
await removeConversation(client, {
conversationSid: newConversationSid,
});
}
throw cause;
}
const { conversationSid: newConversationSid } = createConversationResult.data;

conversationSid = newConversationSid;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export const instagramToFlexHandler: AccountScopedHandler = async (
const channelType = AseloCustomChannel.Instagram;
const chatFriendlyName = `${channelType}:${senderExternalId}`;
const uniqueUserName = `${channelType}:${senderExternalId}`;
const senderScreenName = uniqueUserName; // TODO: see if we can use ig handle somehow
const senderScreenName = senderExternalId; // TODO: see if we can use ig handle somehow
const messageAttributes = JSON.stringify({ messageExternalId });
const onMessageSentWebhookUrl = `${process.env.WEBHOOK_BASE_URL}/lambda/twilio/account-scoped/${accountSid}/customChannels/instagram/flexToInstagram?recipientId=${senderExternalId}`;
const studioFlowSid = await getChannelStudioFlowSid(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export const lineToFlexHandler: AccountScopedHandler = async (
const senderExternalId = messageEvent.source.userId; // The child ID on Line
const chatFriendlyName = `${channelType}:${senderExternalId}`;
const uniqueUserName = `${channelType}:${senderExternalId}`;
const senderScreenName = 'child';
const senderScreenName = senderExternalId;
const onMessageSentWebhookUrl = `${process.env.WEBHOOK_BASE_URL}/lambda/twilio/account-scoped/${accountSid}/customChannels/line/flexToLine?recipientId=${senderExternalId}`;
console.debug(
'LineToFlex: sending message from',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const modicaToFlexHandler: AccountScopedHandler = async (
const senderExternalId = source; // The child phone number
const chatFriendlyName = senderExternalId;
const uniqueUserName = `${channelType}:${senderExternalId}`;
const senderScreenName = 'child';
const senderScreenName = senderExternalId;
const onMessageSentWebhookUrl = `${process.env.WEBHOOK_BASE_URL}/lambda/twilio/account-scoped/${accountSid}/customChannels/modica/flexToModica?recipientId=${senderExternalId}`;
const studioFlowSid = await getChannelStudioFlowSid(
accountSid,
Expand Down
Loading