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
33 changes: 32 additions & 1 deletion commons/src/connections/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import {
handleError,
handleResponse,
getMiddlewareApiUrl,
getDashboardApiUrl,
} from "../methods/api";
import type { FileQuery, MiddlewareResponse } from "@commons/types/Nylas";

export const downloadFile = async (query: FileQuery): Promise<string> => {
let queryString = `${getMiddlewareApiUrl(query.component_id)}/files/${
const queryString = `${getMiddlewareApiUrl(query.component_id)}/files/${
query.file_id
}/download`;

Expand All @@ -16,3 +17,33 @@ export const downloadFile = async (query: FileQuery): Promise<string> => {
.then((json) => json.response)
.catch((error) => handleError(query.component_id, error));
};

export const streamDownloadFile = async ({
file_id,
component_id,
access_token,
}: {
[key: string]: string;
}): Promise<Blob> => {
const baseUrl = getDashboardApiUrl(component_id);
const url = `${baseUrl}/components/files/${file_id}/download`;

const response = await fetch(url, {
// replace with your actual download endpoint
method: "GET",
headers: {
"Content-Type": "application/json",
"X-Component-Id": component_id || "", // Component ID is passed as header
"X-Access-Token": access_token || "", // Access Token is passed as header
Authorization: `Bearer ${access_token || ""}`,
},
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const blob = await response.blob();

return blob;
};
20 changes: 20 additions & 0 deletions commons/src/methods/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,26 @@ export function getMiddlewareApiUrl(id: string): string {
return API_GATEWAY;
}

export function getDashboardApiUrl(id: string): string {
if (process.env.NODE_ENV === "development") {
return `http://localhost:4000`;
}

let region = "";
if (id.substring(3, 4) === "-") {
const code = id.substring(0, 3);
if (typeof REGION_MAPPING[code] !== "undefined") {
region = REGION_MAPPING[code];
}
}

const baseUrl = region.includes("ireland")
? "https://dashboard-api-gateway.eu.nylas.com"
: "https://dashboard-api-gateway.us.nylas.com";

return baseUrl;
}

export function silence(error: Error) {}

export function buildQueryParams(params: Record<string, any>): string {
Expand Down
17 changes: 13 additions & 4 deletions commons/src/methods/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,19 @@ export default function parseStringToArray(parseStr: string): string[] {
return [parseStr.trim()];
}

export function downloadAttachedFile(fileData: string, file: File): void {
const buffer = Uint8Array.from(atob(fileData), (c) => c.charCodeAt(0));
const blob = new Blob([buffer], { type: file.content_type });
const blobFile = window.URL.createObjectURL(blob);
export function downloadAttachedFile(
fileData: string | Blob,
file: File,
): void {
let blobFile;

if (typeof fileData === "string") {
const buffer = Uint8Array.from(atob(fileData), (c) => c.charCodeAt(0));
const blob = new Blob([buffer], { type: file.content_type });
blobFile = window.URL.createObjectURL(blob);
} else if (typeof fileData !== "string") {
blobFile = window.URL.createObjectURL(fileData);
}

const a = document.createElement("a");
a.href = blobFile;
Expand Down
24 changes: 18 additions & 6 deletions commons/src/store/files.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { writable } from "svelte/store";
import type { File, Message } from "@commons/types/Nylas";
import { downloadFile } from "@commons/connections/files";
import { downloadFile, streamDownloadFile } from "@commons/connections/files";
import { InlineImageTypes } from "@commons/constants/attachment-content-types";
function initializeFilesForMessage() {
const { subscribe, set, update } = writable<
Expand All @@ -25,14 +25,26 @@ function initializeFilesForMessage() {
!inlineFiles[file.id]
) {
inlineFiles[file.id] = file;
inlineFiles[file.id].data = await downloadFile({
file_id: file.id,
component_id: query.component_id,
access_token: query.access_token,
});

if (file.size > 4194304) {
const blob = await streamDownloadFile({
file_id: file.id,
component_id: query.component_id,
access_token: query.access_token,
});

inlineFiles[file.id].data = blob;
} else {
inlineFiles[file.id].data = await downloadFile({
file_id: file.id,
component_id: query.component_id,
access_token: query.access_token,
});
}
}
}
filesMap[incomingMessage.id] = inlineFiles;

update((files) => {
files[incomingMessage.id] = inlineFiles;
return { ...files };
Expand Down
2 changes: 1 addition & 1 deletion commons/src/types/Nylas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export interface File {
size: number;
content_disposition: string;
content_id?: string;
data?: string;
data?: string | Blob;
}

export interface MiddlewareResponse<T = unknown> {
Expand Down
62 changes: 51 additions & 11 deletions components/email/src/Email.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
import { FolderStore } from "@commons/store/folders";
import * as DOMPurify from "dompurify";
import LoadingIcon from "./assets/loading.svg";
import { downloadFile } from "@commons/connections/files";
import { streamDownloadFile } from "@commons/connections/files";
import ReplyIcon from "./assets/reply.svg";
import ReplyAllIcon from "./assets/reply-all.svg";
import ForwardIcon from "./assets/forward.svg";
Expand Down Expand Up @@ -91,6 +91,7 @@
export let you: Partial<Account>;
export let show_reply: boolean;
export let show_reply_all: boolean;

export let show_forward: boolean;

const defaultValueMap: Partial<EmailProperties> = {
Expand Down Expand Up @@ -241,7 +242,7 @@
}

let main: Element;
let messageRefs: Element[] = [];
let messageRefs: HTMLElement[] = [];
const MAX_DESKTOP_PARTICIPANTS = 2;
const MAX_MOBILE_PARTICIPANTS = 1;

Expand Down Expand Up @@ -478,7 +479,7 @@
* individual messages to trash folder as a workaround
**/
if (query.component_id && _this.thread_id) {
activeThread.messages.forEach(async (message, i) => {
activeThread.messages.forEach(async (message) => {
await updateMessage(
query.component_id,
{ ...message, folder_id: trashFolderID },
Expand Down Expand Up @@ -704,14 +705,18 @@
}
}

function fetchIndividualMessage(messageID: string): Promise<Message | null> {
async function fetchIndividualMessage(
messageID: string,
): Promise<Message | null> {
if (id) {
return fetchMessage(query, messageID).then(async (json) => {
if (FilesStore.hasInlineFiles(json)) {
const messageWithInlineFiles = await getMessageWithInlineFiles(json);

dispatchEvent("messageLoaded", messageWithInlineFiles);
return messageWithInlineFiles;
}

dispatchEvent("messageLoaded", json);
return json;
});
Expand Down Expand Up @@ -898,13 +903,15 @@

function initializeAttachedFiles() {
const messageType = getMessageType(activeThread);

attachedFiles = activeThread[messageType]?.reduce(
(files: Record<string, File[]>, message) => {
for (const [fileIndex, file] of message.files.entries()) {
if (isFileAnAttachment(file)) {
if (!files[message.id]) {
files[message.id] = [];
}

files[message.id] = [
...files[message.id],
message.files[fileIndex],
Expand All @@ -923,11 +930,31 @@
access_token,
});
for (const file of Object.values(fetchedFiles)) {
if (message.body) {
message.body = message.body?.replaceAll(
`src="cid:${file.content_id}"`,
`src="data:${file.content_type};base64,${file.data}"`,
);
let dataUrl: string | null = null;

if (typeof file.data !== "string") {
const reader = new FileReader();
reader.onload = function (event) {
dataUrl = event.target.result as string;
};
reader.onloadend = function () {
const rawData = dataUrl.split("base64,")[1];

if (message.body) {
message.body = message.body?.replaceAll(
`src="cid:${file.content_id}"`,
`src="data:${file.content_type};base64,${rawData}"`,
);
}
};
reader.readAsDataURL(file.data);
} else if (typeof file.data === "string") {
if (message.body) {
message.body = message.body?.replaceAll(
`src="cid:${file.content_id}"`,
`src="data:${file.content_type};base64,${dataUrl ?? file.data}"`,
);
}
}
}
return message;
Expand All @@ -936,7 +963,7 @@
async function downloadSelectedFile(event: MouseEvent, file: File) {
event.stopImmediatePropagation();
if (id && ((activeThread && _this.thread_id) || _this.message_id)) {
const downloadedFileData = await downloadFile({
const downloadedFileData = await streamDownloadFile({
file_id: file.id,
component_id: id,
access_token,
Expand All @@ -952,7 +979,20 @@

async function handleDownloadFromMessage(event: MouseEvent) {
const file = (<any>event.detail).file;
downloadSelectedFile(event, file);

if (file.data instanceof Blob) {
const url = URL.createObjectURL(file.data);

const link = document.createElement("a");
link.href = url;
link.download = file.filename; // Use the file name or 'download' if the name is not available

document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
downloadSelectedFile(event, file);
}
}

function isThreadADraftEmail(currentThread: Thread): boolean {
Expand Down
20 changes: 10 additions & 10 deletions components/email/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@


"@types/dompurify@^2.3.1":
version "2.3.1"
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.3.1.tgz#2934adcd31c4e6b02676f9c22f9756e5091c04dd"
integrity sha512-YJth9qa0V/E6/XPH1Jq4BC8uCMmO8V1fKWn8PCvuZcAhMn7q0ez9LW6naQT04UZzjFfAPhyRMZmI2a2rbMlEFA==
"integrity" "sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg=="
"resolved" "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz"
"version" "2.4.0"
dependencies:
"@types/trusted-types" "*"

"@types/trusted-types@*":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
"integrity" "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
"resolved" "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz"
"version" "2.0.7"

dompurify@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.3.tgz#c1af3eb88be47324432964d8abc75cf4b98d634c"
integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg==
"dompurify@^2.3.3":
"integrity" "sha512-FgbqnEPiv5Vdtwt6Mxl7XSylttCC03cqP5ldNT2z+Kj0nLxPHJH4+1Cyf5Jasxhw93Rl4Oo11qRoUV72fmya2Q=="
"resolved" "https://registry.npmjs.org/dompurify/-/dompurify-2.5.5.tgz"
"version" "2.5.5"