Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Twilio Conversations & Paste to latest #27

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
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
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
"dependencies": {
"@emotion/core": "^10.0.28",
"@sendgrid/mail": "7.7.0",
"@twilio-paste/core": "^10.20.0",
"@twilio-paste/icons": "^6.1.0",
"@twilio-paste/theme": "^5.3.3",
"@twilio/conversations": "2.1.0-rc.0",
"@twilio-paste/core": "^16.0.0",
"@twilio-paste/icons": "^9.3.0",
"@twilio-paste/theme": "^8.0.2",
"@twilio/conversations": "^2.2.0",
"@types/file-saver": "2.0.5",
"file-saver": "2.0.5",
"google-auth-library": "8.5.1",
Expand Down Expand Up @@ -88,4 +88,4 @@
"engines": {
"node": ">=14"
}
}
}
7 changes: 4 additions & 3 deletions src/__mocks__/@twilio/conversations/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import type { ClientOptions, Client as ClientType, ConnectionState, User as User
import { Conversation } from "./conversation";
import { MockedPaginator } from "../../../test-utils";

const { Client: ConversationClient, User } =
jest.requireActual<{ Client: typeof ClientType; User: typeof UserType }>("@twilio/conversations");
const { Client: ConversationClient, User } = jest.requireActual<{ Client: typeof ClientType; User: typeof UserType }>(
"@twilio/conversations"
);

export class Client extends ConversationClient {
/**
Expand Down Expand Up @@ -71,7 +72,7 @@ export class Client extends ConversationClient {
* Update the token used by the client and re-register with the Conversations services.
* @param token New access token.
*/
async updateToken(): Promise<Client> {
async updateToken(token: string): Promise<Client> {
return this;
}

Expand Down
52 changes: 28 additions & 24 deletions src/__mocks__/@twilio/conversations/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,26 @@ import type {
ParticipantBindingOptions,
SendEmailOptions,
SendMediaOptions,
Conversation as ConversationType
Conversation as ConversationType,
JSONValue
} from "@twilio/conversations";
import { ParticipantResponse } from "../../../definitions";
import { SyncDocument } from "twilio-sync";

import { MockedPaginator } from "../../../test-utils";

const { Conversation: OriginalConversation } =
jest.requireActual<{ Conversation: typeof ConversationType }>("@twilio/conversations");
const { Conversation: OriginalConversation } = jest.requireActual<{ Conversation: typeof ConversationType }>(
"@twilio/conversations"
);

export class Conversation extends OriginalConversation {
/**
* Add a participant to the conversation by its identity.
* @param identity Identity of the Client to add.
* @param attributes Attributes to be attached to the participant.
*/
async add(identity: string, attributes?: Record<string, unknown>): Promise<void> {
return Promise.resolve();
async add(identity: string, attributes?: JSONValue): Promise<ParticipantResponse> {
return Promise.resolve() as unknown as ParticipantResponse;
}

/**
Expand All @@ -36,10 +40,10 @@ export class Conversation extends OriginalConversation {
async addNonChatParticipant(
proxyAddress: string,
address: string,
attributes?: Record<string, unknown>,
attributes?: JSONValue,
bindingOptions?: ParticipantBindingOptions
): Promise<void> {
return Promise.resolve();
): Promise<ParticipantResponse> {
return Promise.resolve() as unknown as ParticipantResponse;
}

/**
Expand All @@ -56,14 +60,14 @@ export class Conversation extends OriginalConversation {
/**
* Delete the conversation and unsubscribe from its events.
*/
async delete(): Promise<Conversation> {
return this as unknown as Conversation;
async delete(): Promise<ConversationType> {
return this as unknown as ConversationType;
}

/**
* Get the custom attributes of this Conversation.
*/
async getAttributes(): Promise<Record<string, unknown>> {
async getAttributes(): Promise<JSONValue> {
return Promise.resolve({});
}

Expand Down Expand Up @@ -156,15 +160,15 @@ export class Conversation extends OriginalConversation {
/**
* Join the conversation and subscribe to its events.
*/
async join(): Promise<Conversation> {
return this;
async join(): Promise<ConversationType> {
return this as unknown as ConversationType;
}

/**
* Leave the conversation.
*/
async leave(): Promise<Conversation> {
return this;
async leave(): Promise<ConversationType> {
return this as unknown as ConversationType;
}

/**
Expand All @@ -186,7 +190,7 @@ export class Conversation extends OriginalConversation {
*/
async sendMessage(
message: string | FormData | SendMediaOptions | null,
messageAttributes?: Record<string, unknown>,
messageAttributes?: JSONValue,
emailOptions?: SendEmailOptions
): Promise<number> {
return Promise.resolve(1);
Expand Down Expand Up @@ -237,16 +241,16 @@ export class Conversation extends OriginalConversation {
* Update the attributes of the conversation.
* @param attributes New attributes.
*/
async updateAttributes(attributes: Record<string, unknown>): Promise<Conversation> {
return this;
async updateAttributes(attributes: JSONValue): Promise<ConversationType> {
return this as unknown as ConversationType;
}

/**
* Update the friendly name of the conversation.
* @param friendlyName New friendly name.
*/
async updateFriendlyName(friendlyName: string): Promise<Conversation> {
return this;
async updateFriendlyName(friendlyName: string): Promise<ConversationType> {
return this as unknown as ConversationType;
}

/**
Expand All @@ -263,17 +267,17 @@ export class Conversation extends OriginalConversation {
* Update the unique name of the conversation.
* @param uniqueName New unique name for the conversation. Setting unique name to null removes it.
*/
async updateUniqueName(uniqueName: string | null): Promise<Conversation> {
return this;
async updateUniqueName(uniqueName: string | null): Promise<ConversationType> {
return this as unknown as ConversationType;
}

/**
* Load and subscribe to this conversation and do not subscribe to its participants and messages.
* This or _subscribeStreams will need to be called before any events on conversation will fire.
* @internal
*/
async _subscribe(): Promise<unknown> {
return Promise.resolve({});
async _subscribe(): Promise<SyncDocument> {
return Promise.resolve({}) as unknown as SyncDocument;
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/components/FilePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Box } from "@twilio-paste/core/box";
import { Text } from "@twilio-paste/core/text";
import { FileIcon } from "@twilio-paste/icons/esm/FileIcon";
import { CloseIcon } from "@twilio-paste/icons/esm/CloseIcon";
import { Button } from "@twilio-paste/core/button";
import { Media } from "@twilio/conversations";
import { extension as mimeToExtension } from "mime-types";
import { Truncate } from "@twilio-paste/core/truncate";
import { CloseIcon } from "@twilio-paste/icons/esm/CloseIcon";
import { FileIcon } from "@twilio-paste/icons/esm/FileIcon";

import { addNotification, detachFiles } from "../store/actions/genericActions";
import { AppState } from "../store/definitions";
Expand Down Expand Up @@ -72,7 +72,7 @@ export const FilePreview = (props: FilePreviewProps) => {

try {
const url = media ? await media.getContentTemporaryUrl() : URL.createObjectURL(file);
window.open(url);
window.open(url as string | undefined);
} catch (e) {
log.error(`Failed downloading message attachment: ${e}`);
}
Expand Down
14 changes: 10 additions & 4 deletions src/components/MessageBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const MessageBubble = ({
}
};

const author = users?.find((u) => u.identity === message.author)?.friendlyName || message.author;
const author: string | null = users?.find((u) => u.identity === message.author)?.friendlyName || message.author;

return (
<Box
Expand All @@ -131,7 +131,13 @@ 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={author}>
<Text
{...authorStyles}
as="p"
aria-hidden
style={{ textOverflow: "ellipsis" }}
title={author as unknown as string | undefined}
>
{author}
</Text>
<ScreenReaderOnly as="p">
Expand All @@ -140,8 +146,8 @@ export const MessageBubble = ({
: `${users?.find((u) => u.identity === message.author)?.friendlyName} sent at`}
</ScreenReaderOnly>
<Text {...timeStampStyles} as="p">
{`${doubleDigit(message.dateCreated.getHours())}:${doubleDigit(
message.dateCreated.getMinutes()
{`${doubleDigit(message.dateCreated?.getHours() || 0)}:${doubleDigit(
message.dateCreated?.getMinutes() || 0
)}`}
</Text>
</Flex>
Expand Down
2 changes: 1 addition & 1 deletion src/components/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export const MessageList = () => {
Chat started
</Text>
<Text as="p" {...conversationEventDateStyles}>
{conversation?.dateCreated.toLocaleString()}
{conversation?.dateCreated?.toLocaleString()}
</Text>
</Box>
</>
Expand Down
2 changes: 1 addition & 1 deletion src/components/MessageListSeparator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const MessageListSeparator = ({
} else if (daysOld === 1) {
separatorText = "Yesterday";
} else {
separatorText = message.dateCreated.toLocaleDateString();
separatorText = message?.dateCreated?.toLocaleDateString();
}
}

Expand Down
21 changes: 12 additions & 9 deletions src/components/__tests__/NotificationBarItem.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fireEvent, render } from "@testing-library/react";
import { fireEvent, queryHelpers, render } from "@testing-library/react";
import "@testing-library/jest-dom";

import { NotificationBarItem } from "../NotificationBarItem";
Expand All @@ -9,6 +9,8 @@ jest.mock("react-redux", () => ({
useDispatch: () => jest.fn()
}));

export const queryByPasteElement = queryHelpers.queryByAttribute.bind(null, "data-paste-element");

describe("Notification Bar Item", () => {
const notification: Notification = {
message: "Test notification",
Expand All @@ -18,6 +20,7 @@ describe("Notification Bar Item", () => {
};

const dismissButtonTitle = "Dismiss alert";
const dismissPasteElementName = "ALERT_DISMISS_BUTTON";

it("renders a notification bar item", () => {
const { container } = render(<NotificationBarItem {...notification} />);
Expand All @@ -32,9 +35,9 @@ describe("Notification Bar Item", () => {
});

it("renders a dismiss button if dismissible is true", () => {
const { queryByText } = render(<NotificationBarItem {...notification} dismissible={true} />);
const { container } = render(<NotificationBarItem {...notification} dismissible={true} />);

expect(queryByText(dismissButtonTitle)).toBeInTheDocument();
expect(queryByPasteElement(container, dismissPasteElementName)).toBeInTheDocument();
});

it("does not render a dismiss button if dismissible is false", () => {
Expand All @@ -45,22 +48,22 @@ describe("Notification Bar Item", () => {

it("dismisses notification when dismiss button is clicked", () => {
const removeNotificationSpy = jest.spyOn(genericActions, "removeNotification");
const { getByTitle } = render(<NotificationBarItem {...notification} dismissible={true} />);
const { container } = render(<NotificationBarItem {...notification} dismissible={true} />);

const dismissButton = getByTitle(dismissButtonTitle);
fireEvent.click(dismissButton);
const dismissButton = queryByPasteElement(container, dismissPasteElementName);
fireEvent.click(dismissButton as unknown as Element);

expect(removeNotificationSpy).toHaveBeenCalledWith(notification.id);
});

it("runs onDismiss function prop when dismiss button is clicked", () => {
const onDismiss = jest.fn();
const { getByTitle } = render(
const { container } = render(
<NotificationBarItem {...notification} dismissible={true} onDismiss={onDismiss} />
);

const dismissButton = getByTitle(dismissButtonTitle);
fireEvent.click(dismissButton);
const dismissButton = queryByPasteElement(container, dismissPasteElementName);
fireEvent.click(dismissButton as unknown as Element);

expect(onDismiss).toHaveBeenCalled();
});
Expand Down
21 changes: 21 additions & 0 deletions src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,24 @@ export type TranscriptConfig = {
emailSubject?: (agentNames: (string | undefined)[]) => string;
emailContent?: (customerName: string | undefined, transcript: string) => string;
};

export interface ParticipantResponse {
account_sid: string;
chat_service_sid: string;
conversation_sid: string;
role_sid: string;
sid: string;
attributes: string;
date_created: string;
date_updated: string;
identity: string;
messaging_binding: {
type: "chat" | "sms" | "whatsapp" | "email";
address: string;
proxy_address: string;
} | null;
url: string;
links: {
conversation: string;
};
}
Comment on lines +21 to +40
Copy link
Contributor

Choose a reason for hiding this comment

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

Wondering if it's necessary to define this purely for the mock file's benefit. Was this because the new Conversations version required it?

5 changes: 4 additions & 1 deletion src/utils/getDaysOld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
* Gets the number of days between the given date and the current date.
* e.g. if today is 10/01/2022 then 08/01/2022 returns 2
*/
export const getDaysOld = (date: Date): number => {
export const getDaysOld = (date: Date | null): number => {
if (date === null) {
return 0;
}
const messageDate = new Date(date.getTime());
messageDate.setUTCHours(0);
messageDate.setUTCMinutes(0);
Expand Down