diff --git a/commons/src/methods/contact_strings.ts b/commons/src/methods/contact_strings.ts new file mode 100644 index 00000000..c4395636 --- /dev/null +++ b/commons/src/methods/contact_strings.ts @@ -0,0 +1,33 @@ +import type { Participant, Account } from "@commons/types/Nylas"; + +export function getNameInitials(name: string): string { + const nameParts = name.split(" "); + return nameParts + .map((n, i) => (i === 0 || i === nameParts.length - 1 ? n[0] : "")) + .join(""); +} + +export function getContactInitialForAvatar( + contact: Participant | Partial, +): string { + const participant = contact; + const account = >contact; + + // if participant type + if (participant.email) { + if (participant.name) { + return getNameInitials(participant.name); + } + return participant.email[0]; + } + + // else account type + // since this is partial, need to check every attr and have a fallback + if (account.name) { + return getNameInitials(account.name); + } else if (account.email_address) { + return account.email_address[0]; + } else { + return "?"; + } +} diff --git a/commons/src/methods/datetime.ts b/commons/src/methods/datetime.ts new file mode 100644 index 00000000..b24b29aa --- /dev/null +++ b/commons/src/methods/datetime.ts @@ -0,0 +1,51 @@ +export function getTimeString(date: Date): string { + // 11:11am + return date + .toLocaleTimeString([], { timeStyle: "short" }) + .replaceAll(/\./g, ""); +} + +export function getYearString(date: Date): string { + // Jul 5th 2020 + const suffixMapper = (num: number): string => { + if ([31, 21, 1].includes(num)) return "st "; + if ([22, 2].includes(num)) return "nd "; + if ([23, 3].includes(num)) return "rd "; + return "th "; + }; + return date + .toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }) + .replaceAll(/, /g, suffixMapper(date.getDate())) + .replaceAll(/[.]/g, ""); +} + +export function getDateString(date: Date): string { + // Sep 22 + return date + .toLocaleDateString(undefined, { month: "short", day: "numeric" }) + .replaceAll(/[.]/g, ""); +} + +// Date logic: https://www.figma.com/file/oiCKNsHDfAo9KnH1Sbs8Xj/Email-%26-Mailbox-Component?node-id=128%3A51 +export function getDate(date: Date): string { + const today = new Date(); + if (today.toDateString() === date.toDateString()) { + return getTimeString(date); + } + + const diff_years = today.getFullYear() - date.getFullYear(); + if (diff_years !== 0) { + return getYearString(date); + } + + const yesterday = new Date(today.getDate() - 1); + if (yesterday.toDateString() === date.toDateString()) { + return "Yesterday"; + } + + return getDateString(date); +} diff --git a/commons/src/store/events.ts b/commons/src/store/events.ts index 8ec78a70..290590dc 100644 --- a/commons/src/store/events.ts +++ b/commons/src/store/events.ts @@ -26,9 +26,9 @@ function initializeEvents() { return await eventsMap[queryKey]; } }, - createEvent: (event: Event, query: EventQuery) => { + createEvent: async (event: Event, query: EventQuery) => { const queryKey = JSON.stringify(query); - if (eventsMap[queryKey]) { + if (await eventsMap[queryKey]) { eventsMap[queryKey] = Promise.all([ eventsMap[queryKey], createEvent(event, query), diff --git a/components/conversation/src/Conversation.svelte b/components/conversation/src/Conversation.svelte index f738ed47..e3c62d54 100644 --- a/components/conversation/src/Conversation.svelte +++ b/components/conversation/src/Conversation.svelte @@ -25,7 +25,8 @@ Conversation, Account, } from "@commons/types/Nylas"; - import "../../contacts-search/src/ContactsSearch.svelte"; + import { getDate } from "@commons/methods/datetime"; + import { getContactInitialForAvatar } from "@commons/methods/contact_strings"; export let id: string = ""; export let access_token: string = ""; @@ -92,6 +93,7 @@ let conversation: Conversation | null = null; let status: "loading" | "loaded" | "error" = "loading"; + let headerExpanded = false; $: { if (id && thread_id) { @@ -156,10 +158,11 @@ let replyBody = ""; - const handleContactsChange = - (field: "to" | "from" | "cc") => (data: Participant[]) => { - reply[field] = data; - }; + const handleContactsChange = (field: "to" | "from" | "cc") => ( + data: Participant[], + ) => { + reply[field] = data; + }; let lastMessage: Message; let lastMessageInitialised = false; @@ -240,58 +243,92 @@ @import "../../theming/variables.scss"; @import "../../theming/themes.scss"; + $tabletBreakpoint: 768px; + $desktopBreakpoint: 1140px; $contactWidth: 32px; + $avatar-size: 40px; + $headerHorizontalSpacing: 32px; + $avatar-horizontal-space: 1rem; + main { height: 100%; width: 100%; overflow: auto; position: relative; - background-color: #eee; + font-family: sans-serif; + background-color: var(--grey-light); + } + + header { + display: flex; + background: white; + padding: 15px $headerHorizontalSpacing; + gap: $headerHorizontalSpacing; + color: var(--black); + font-size: var(--fs-14); + position: fixed; + width: 100%; + top: 0; + z-index: 1; + &.mobile { + @media (min-width: $tabletBreakpoint) { + display: none; + } + width: calc(100% - (#{$headerHorizontalSpacing} * 2)); + button { + position: absolute; + right: $headerHorizontalSpacing; + top: 16px; + background: none; + display: flex; + } + &.expanded { + display: grid; + gap: 12px; + button { + rotate: 180deg; + } + } + } + &.tablet { + display: none; + @media (min-width: $tabletBreakpoint) { + display: flex; + } + } } - $avatar-size: 32px; - $min-horizontal-space-between-participants: 4rem; .messages { display: grid; gap: 1rem; padding: 1rem; + padding-top: calc(1rem + 15px + 15px + 15px); + padding-bottom: calc(25px + 12px + 12px); .message { - width: clamp( - 200px, - calc( - 100% - #{$avatar-size} - #{$min-horizontal-space-between-participants} - ), - 700px + max-width: min( + 400px, + calc(100% - #{$avatar-size} - #{$avatar-horizontal-space}) ); + @media (min-width: $tabletBreakpoint) { + width: max-content; + max-width: 600px; + } + @media (min-width: $desktopBreakpoint) { + max-width: 752px; + } display: grid; - column-gap: 1rem; + column-gap: $avatar-horizontal-space; row-gap: 0.25rem; grid-template-columns: $contactWidth 1fr; + grid-template-rows: auto auto; transition: 0.5s; &:last-child { padding-bottom: 2rem; } - - header { - grid-column: -1 / 2; - display: grid; - grid-template-columns: 1fr 1fr; - font-size: 0.8rem; - opacity: 0.75; - overflow: hidden; - .date { - text-align: right; - } - &.hidden { - display: none; - } - } .body { - border-radius: 4px; + border-radius: 8px; background-color: white; - border-bottom: 10px solid; - box-shadow: 1px 1px 30px rgba(0, 0, 0, 0.05); - border-color: inherit; + color: var(--black); max-height: 50vh; overflow: auto; position: relative; @@ -303,7 +340,7 @@ grid-template-rows: auto 1fr; gap: 0.5rem; .avatar { - border-radius: 16px; + border-radius: 20px; width: $avatar-size; height: $avatar-size; text-align: center; @@ -319,6 +356,10 @@ width: 100%; height: 100%; } + span { + // perfectly center text + padding-top: 4px; + } } .email { font-size: 0.8rem; @@ -328,47 +369,51 @@ text-overflow: ellipsis; } } - + .time { + grid-column: 2/3; + font-size: var(--fs-12); + color: var(--grey-dark); + } p { padding: 1rem; - color: black; font-weight: 300; line-height: 1.3em; font-size: 0.9em; border-radius: 8px; white-space: pre-line; // maintains newlines from conversation - &.snippet { - color: rgba(0, 0, 0, 0.5); - &:before { - content: "Expanding your message; please wait..."; - color: rgba(0, 0, 0, 1); - } + &.after { + padding-top: 0; + margin-top: -1rem; + color: var(--grey-dark); } } &.you { justify-self: end; grid-template-columns: 1fr $contactWidth; - - header { - grid-column: 1 / 1; - } - .contact { - order: 2; + grid-row: 1/2; + grid-column: 2/3; } .body { order: 1; grid-column: 1 / 1; + color: var(--white); + background-color: var(--blue); + p.after { + color: var(--grey); + } + } + .time { + order: 1; + grid-column: 1 / 1; } } } - &.dont-show-avatars { .message { column-gap: 0; grid-template-columns: 0 1fr; width: clamp(200px, calc(100% - 4rem), 700px); - .contact { overflow: hidden; } @@ -377,46 +422,42 @@ } } } + } - .reply { - background: #ddd; - padding: 1rem; - margin: 1rem -1rem -1rem; - form { - button[type="submit"] { - &:disabled { - cursor: not-allowed; - background: gray; - } - } - } - span.to { - --background: gray; - } - span.to, - span.cc { - display: inline-block; - padding: 0 1rem 0 0; - - & > span { - display: inline-block; - margin-right: 0.5rem; + .reply-box { + position: fixed; + width: 100%; + bottom: 0; + z-index: 1; + form { + position: relative; + display: flex; + align-items: center; + button[type="submit"] { + position: absolute; + right: 1rem; + border-radius: 4px; + background-color: var(--blue); + height: 28px; + width: 28px; + color: white; + display: flex; + align-items: center; + justify-content: center; + &:disabled { + cursor: not-allowed; + background-color: gray; } } - - .response { + input { + border-top: 1px solid #ebebeb; + height: 25px; + padding: 12px 1rem; width: 100%; - display: grid; - grid-template-columns: 1fr 100px; - gap: 1rem; - margin-top: 0.5rem; - input { - padding: 0.5rem; - } - button { - background-color: black; - font-weight: 500; - color: white; + font-size: var(--fs-16); + color: var(--grey-black); + &::placeholder { + color: var(--grey); } } } @@ -428,6 +469,44 @@ {#await conversation} Loading Component... {:then _} +
+ {#if reply.to.length} + to: {reply.to[0].email} + {/if} + {#if reply.to.length > 1 || reply.cc.length} + + {/if} + {#if headerExpanded} + + {#each reply.to.slice(1) as contact} + to: {contact.email} + {/each} + {#each reply.cc as contact} + cc: {contact.email} + {/each} + {/if} +
+
+ {#if reply.to.length} + to: {reply.to.map((p) => p.email).join(", ")} + {/if} + {#if reply.cc.length} + cc: {reply.cc.map((p) => p.email).join(", ")} + {/if} +
{#if status === "loading"}Loading Messages...{/if}
{#each messages as message, i} @@ -439,25 +518,6 @@ class="message member-{participantIndex + 1}" class:you={isYou} > -
- - - - {#if new Date().toDateString() === new Date(message.date * 1000).toDateString()} - {new Date(message.date * 1000).toLocaleTimeString()} - {:else} - {new Date(message.date * 1000).toLocaleDateString()} - {/if} - -
{#await (participants[participantIndex] || {}).contact then contact}
@@ -478,12 +538,12 @@ {/if} {:else if contact.given_name && contact.surname} {contact.given_name[0] + contact.surname[0]} - {:else}{contact.emails[0].email.slice(0, 2)}{/if} - {:else if from.email === you.name} - {you.name.slice(0, 2)} - {:else if from.name} - {from.name.slice(0, 2)} - {:else if from.email}{from.email.slice(0, 2)}{/if} + {:else}{contact.emails[0].email[0]}{/if} + {:else if isYou} + {getContactInitialForAvatar(you)} + {:else} + {getContactInitialForAvatar(from)} + {/if}
{/await}
@@ -495,74 +555,80 @@

{@html message.body}

+ {:else if message.snippet.includes(" On ")} +

{message.snippet.split("On ")[0]}

+

On {message.snippet.split("On ")[1]}

{:else} -

{message.snippet}

+

{message.snippet}

{/if}
+
+ {getDate(new Date(message.date * 1000))} +
{/await} {/await} {/await} {/await} {/each} - {#if show_reply} -
-
{ - if (replyStatus !== "sending") { - e.preventDefault(); - replyStatus = "sending"; - if (!conversation) { - return; - } - sendMessage(id, { - from: reply.from, - to: reply.to, - body: `${replyBody}

--Sent with Nylas`, - subject: conversation.subject, - cc: reply.cc, - reply_to_message_id: lastMessage.id, - bcc: [], - }).then((res) => { - const conversationQuery = { queryKey: queryKey, data: res }; - ConversationStore.addMessageToThread(conversationQuery); - replyStatus = ""; - replyBody = ""; - }); +
+ {#if show_reply} +
+ { + if (replyStatus !== "sending") { + e.preventDefault(); + replyStatus = "sending"; + if (!conversation) { + return; } - }} + sendMessage(id, { + from: reply.from, + to: reply.to, + body: `${replyBody}

--Sent with Nylas`, + subject: conversation.subject, + cc: reply.cc, + reply_to_message_id: lastMessage.id, + bcc: [], + }).then((res) => { + const conversationQuery = { queryKey: queryKey, data: res }; + ConversationStore.addMessageToThread(conversationQuery); + replyStatus = ""; + replyBody = ""; + }); + } + }} + > + + + - - -
- {/if} - + {#if replyStatus === "sending"}...{:else} + + + + {/if} + + + + {/if} {/await} diff --git a/components/theming/reset.scss b/components/theming/reset.scss index fa127326..b7026d77 100644 --- a/components/theming/reset.scss +++ b/components/theming/reset.scss @@ -7,3 +7,12 @@ vertical-align: baseline; list-style: none; } + +.sr-only { + position: absolute; + left: -10000px; + top: auto; + width: 1px; + height: 1px; + overflow: hidden; +} diff --git a/components/theming/variables.scss b/components/theming/variables.scss index 5b2c1dd5..9cbe7390 100644 --- a/components/theming/variables.scss +++ b/components/theming/variables.scss @@ -12,18 +12,25 @@ $ellsworth-kelly: #f8b800, #4db930, #00a14d, #008874, #0758bb, #093cb3, #58266e, // Responsive Vars $desktop: "(min-width: 640px)"; -// Colours main { + // colours --black: #161717; - --black-90: #2c2e2e; - --grey-dark: #454954; - --grey: #6a7285; - --grey-light: #8d94a5; - --grey-lighter: #e3e8ee; + --grey-dark: #636671; + --grey: #bdc0cb; + --grey-lighter: #dfe1e8; --grey-lightest: #f7f7f8; - --grey-warm: #cbcbcb; + --grey-background: #f0f1f5; --white: #ffffff; - --blue: #002db4; - --blue-lighter: #e3e9ee; + --blue-lighter: #f0f3ff; + + // font sizes + --fs-12: 0.75rem; + --fs-14: 0.875rem; + --fs-16: 1rem; + + // legacy + --black-90: #2c2e2e; + --grey-light: #f7f7f8; + --grey-warm: #cbcbcb; } diff --git a/package.json b/package.json index 0f9ea8a3..ea60b5ac 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "svelte-preprocess": "4.1.1", "ts-jest": "^26.4.4", "tslib": "^2.0.0", - "typescript": "4.1.2" + "typescript": "4.4.2" }, "dependencies": { "@nylas/components-agenda": "file:components/agenda",