Merge pull request #882 from appwrite/feat-messaging-edit-message

Update flow for editing a message
This commit is contained in:
Torsten Dittmann
2024-02-29 12:53:39 +01:00
committed by GitHub
25 changed files with 1253 additions and 530 deletions
+3 -3
View File
@@ -12,9 +12,9 @@
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"test": "vitest run",
"test:ui": "vitest --ui",
"test:watch": "vitest watch",
"test": "TZ=EST vitest run",
"test:ui": "TZ=EST vitest --ui",
"test:watch": "TZ=EST vitest watch",
"e2e": "playwright test tests/e2e"
},
"dependencies": {
+32
View File
@@ -41,6 +41,38 @@ export const toLocaleDateTime = (datetime: string | number) => {
return date.toLocaleDateString('en', options);
};
/**
* Returns a date string usig the local timezone in ISO format (yyyy-mm-dd)
*
* @param datetime date string or milliseconds since the epoch
*/
export const toLocaleDateISO = (datetime: string | number) => {
const date = new Date(datetime);
if (isNaN(date.getTime())) {
return 'n/a';
}
// Use Sweden's locale (sv) since it matches ISO format
return date.toLocaleDateString('sv');
};
/**
* Returns a time string usig the local timezone in ISO format (hh:mm:ss)
*
* @param datetime date string or milliseconds since the epoch
*/
export const toLocaleTimeISO = (datetime: string | number) => {
const date = new Date(datetime);
if (isNaN(date.getTime())) {
return 'n/a';
}
// Use Sweden's locale (sv) since it matches ISO format
return date.toLocaleTimeString('sv');
};
export const isSameDay = (date1: Date, date2: Date) => {
return (
date1.getFullYear() === date2.getFullYear() &&
@@ -7,13 +7,12 @@
import { sdk } from '$lib/stores/sdk';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { addNotification } from '$lib/stores/notifications';
import { goto, invalidate } from '$app/navigation';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { project } from '../store';
import { wizard } from '$lib/stores/wizard';
import { providerType, messageParams, operation } from './wizard/store';
import { providerType, messageParams } from './wizard/store';
import { ID, MessagingProviderType, type Models } from '@appwrite.io/console';
import { Dependencies } from '$lib/constants';
async function create() {
try {
@@ -111,114 +110,9 @@
}
}
async function update() {
try {
let response: Models.Message;
const messageId = $messageParams[$providerType].messageId;
const params = $messageParams[$providerType];
console.log(params);
switch ($providerType) {
case MessagingProviderType.Email:
response = await sdk.forProject.messaging.updateEmail(
messageId,
$messageParams[$providerType].topics,
$messageParams[$providerType].users,
$messageParams[$providerType].targets,
$messageParams[$providerType].subject,
$messageParams[$providerType].content,
$messageParams[$providerType].draft,
$messageParams[$providerType].html,
undefined,
undefined,
$messageParams[$providerType].scheduledAt
);
break;
case MessagingProviderType.Sms:
response = await sdk.forProject.messaging.updateSms(
messageId,
$messageParams[$providerType].topics,
$messageParams[$providerType].users,
$messageParams[$providerType].targets,
$messageParams[$providerType].content,
$messageParams[$providerType].draft,
$messageParams[$providerType].scheduledAt
);
break;
case MessagingProviderType.Push:
{
const customData: Record<string, string> = {};
const { data } = $messageParams[MessagingProviderType.Push];
if (data && data.length > 0) {
data.forEach((item) => {
if (item[0] === '') return;
customData[item[0]] = item[1];
});
}
response = await sdk.forProject.messaging.updatePush(
messageId,
$messageParams[$providerType].topics,
$messageParams[$providerType].users,
$messageParams[$providerType].targets,
$messageParams[$providerType].title,
$messageParams[$providerType].body,
customData,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
$messageParams[$providerType].draft,
$messageParams[$providerType].scheduledAt
);
}
break;
}
wizard.hide();
let message = '';
switch (response.status) {
case 'draft':
message = 'The message has been saved as draft.';
break;
case 'processing':
message = 'The message is queued for processing.';
break;
case 'scheduled':
message = 'The message has been scheduled.';
break;
}
addNotification({
type: 'success',
message
});
await invalidate(Dependencies.MESSAGING_MESSAGE);
trackEvent(Submit.MessagingMessageUpdate, {
providerType: $providerType,
status: response.status
});
await goto(`${base}/console/project-${$project.$id}/messaging/message-${response.$id}`);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.MessagingMessageUpdate);
}
}
async function saveDraft() {
$messageParams[$providerType].draft = true;
if ($operation === 'create') {
create();
} else {
update();
}
create();
}
const stepsComponents: WizardStepsType = new Map();
@@ -254,8 +148,4 @@
});
</script>
<Wizard
title={$operation === 'create' ? 'Create message' : 'Update message'}
steps={stepsComponents}
on:finish={$operation === 'create' ? create : update}
finalAction="Send" />
<Wizard title="Create message" steps={stepsComponents} on:finish={create} finalAction="Send" />
@@ -3,8 +3,8 @@
import { Button } from '$lib/elements/forms';
import { wizard } from '$lib/stores/wizard';
import { providers } from './providers/store';
import Wizard from './wizard.svelte';
import { messageParams, operation, providerType, targetsById } from './wizard/store';
import Create from './create.svelte';
import { messageParams, providerType, targetsById } from './wizard/store';
import { topicsById } from './store';
import { MessagingProviderType } from '@appwrite.io/console';
@@ -30,7 +30,6 @@
)
return;
$providerType = type;
$operation = 'create';
$topicsById = {};
$targetsById = {};
const common = {
@@ -62,7 +61,7 @@
break;
}
showCreateDropdown = false;
wizard.start(Wizard);
wizard.start(Create);
}}>
{option.name}
</DropListItem>
@@ -1,111 +1,39 @@
<script lang="ts">
import { Container } from '$lib/layout';
import Delete from './delete.svelte';
import EmailPreview from './emailPreview.svelte';
import EmailMessage from './emailMessage.svelte';
import Overview from './overview.svelte';
import { message } from './store';
import SMSPreview from './smsPreview.svelte';
import PushPreview from './pushPreview.svelte';
import { messageParams, operation, providerType, targetsById } from '../wizard/store';
import { topicsById } from '../store';
import { wizard } from '$lib/stores/wizard';
import Wizard from '../wizard.svelte';
import SMSMessage from './smsMessage.svelte';
import PushMessage from './pushMessage.svelte';
import { providerType } from '../wizard/store';
import type { PageData } from './$types';
import { isValueOfStringEnum } from '$lib/helpers/types';
import { MessagingProviderType } from '@appwrite.io/console';
import Topics from './topics.svelte';
import Targets from './targets.svelte';
import UpdateTopics from './updateTopics.svelte';
import UpdateTargets from './updateTargets.svelte';
import { onMount } from 'svelte';
export let data: PageData;
async function onEdit() {
if (!isValueOfStringEnum(MessagingProviderType, $message.providerType)) {
throw new Error(`Invalid provider type: ${$message.providerType}`);
onMount(() => {
if (isValueOfStringEnum(MessagingProviderType, $message.providerType)) {
$providerType = $message.providerType;
}
$operation = 'update';
$providerType = $message.providerType;
$topicsById = {};
$targetsById = {};
$topicsById = data.topicsById;
$targetsById = data.targetsById;
$messageParams[$providerType] = {
messageId: $message.$id,
topics: $message.topics,
users: $message.users,
targets: $message.targets,
draft: $message.status === 'draft',
scheduledAt: $message.scheduledAt
};
switch ($providerType) {
case MessagingProviderType.Email:
{
const { data } = $message;
const params = ['subject', 'content', 'html'];
params.forEach((key) => {
if (typeof data[key] !== 'undefined') {
$messageParams[$providerType][key] = data[key];
}
});
}
break;
case MessagingProviderType.Sms:
{
const { data } = $message;
const params = ['content'];
params.forEach((key) => {
if (typeof data[key] !== 'undefined') {
$messageParams[$providerType][key] = data[key];
}
});
}
break;
case MessagingProviderType.Push:
{
const { data } = $message;
const params = [
'title',
'body',
'action',
'icon',
'sound',
'color',
'tag',
'badge'
];
params.forEach((key) => {
if (typeof data[key] !== 'undefined') {
$messageParams[$providerType][key] = data[key];
}
});
const dataEntries: [string, string][] = [];
Object.entries(data['data'] ?? {}).forEach(([key, value]) => {
dataEntries.push([key, value.toString()]);
});
$messageParams[$providerType]['data'] = dataEntries.length
? dataEntries
: [['', '']];
}
break;
}
wizard.start(Wizard);
}
});
</script>
<Container>
<Overview />
<Overview message={$message} topics={Object.values(data.topicsById)} />
{#if $message.providerType === MessagingProviderType.Email}
<EmailPreview message={$message} onEdit={$message.status === 'draft' ? onEdit : null} />
<EmailMessage message={$message} />
{:else if $message.providerType === MessagingProviderType.Sms}
<SMSPreview message={$message} onEdit={$message.status === 'draft' ? onEdit : null} />
<SMSMessage message={$message} />
{:else if $message.providerType === MessagingProviderType.Push}
<PushPreview message={$message} onEdit={$message.status === 'draft' ? onEdit : null} />
<PushMessage message={$message} />
{/if}
<Topics topics={Object.values(data.topicsById)} />
<Targets targets={Object.values(data.targetsById)} usersById={data.usersById} />
<UpdateTopics message={$message} selectedTopicsById={data.topicsById} />
<UpdateTargets message={$message} selectedTargetsById={data.targetsById} />
{#if $message.status !== 'processing'}
<Delete message={$message} />
{/if}
@@ -0,0 +1,95 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { MessagingProviderType, type Models } from '@appwrite.io/console';
import { Dependencies } from '$lib/constants';
export let show = false;
export let message: Models.Message & { data: Record<string, unknown> };
const update = async () => {
try {
if (message.providerType == MessagingProviderType.Email) {
await sdk.forProject.messaging.updateEmail(
message.$id,
undefined,
undefined,
undefined,
undefined,
undefined,
true
);
} else if (message.providerType == MessagingProviderType.Sms) {
await sdk.forProject.messaging.updateSms(
message.$id,
undefined,
undefined,
undefined,
undefined,
true
);
} else if (message.providerType == MessagingProviderType.Push) {
await sdk.forProject.messaging.updatePush(
message.$id,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
true
);
}
await invalidate(Dependencies.MESSAGING_MESSAGE);
addNotification({
message: `The scheduling has been cancelled.`,
type: 'success'
});
trackEvent(Submit.MessagingMessageUpdate, {
providerType: message.providerType,
status
});
show = false;
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.MessagingMessageUpdate);
}
};
</script>
<Modal
title="Cancel scheduling"
bind:show
onSubmit={update}
headerDivider={false}
size="small"
icon="exclamation"
state="warning">
<div class="u-flex-vertical u-gap-16">
<p data-private>
Are you sure you want to cancel the scheduling of <span class="u-bold"
>{message.data.title ??
message.data.subject ??
message.data.content ??
'Message'}</span
>?
</p>
</div>
<svelte:fragment slot="footer">
<Button secondary submit>Cancel scheduling</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,93 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import {
Button,
Form,
FormList,
InputSwitch,
InputText,
InputTextarea
} from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { onMount } from 'svelte';
export let message: Models.Message & { data: Record<string, string> };
let subject = '';
let content = '';
let html = false;
let disabled = true;
onMount(() => {
subject = message.data.subject;
content = message.data.content;
html = (message.data['html'] ?? false) as boolean;
});
async function update() {
try {
await sdk.forProject.messaging.updateEmail(
message.$id,
undefined,
undefined,
undefined,
subject,
content,
undefined,
html
);
await invalidate(Dependencies.MESSAGING_MESSAGE);
addNotification({
message: 'Message has been updated',
type: 'success'
});
trackEvent(Submit.MessagingMessageUpdate);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.MessagingMessageUpdate);
}
}
$: disabled =
subject === message.data.subject &&
content === message.data.content &&
html === ((message.data['html'] ?? false) as boolean);
</script>
<Form onSubmit={update}>
<CardGrid hideFooter={message.status != 'draft'}>
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
<Heading tag="h6" size="7">Message</Heading>
</div>
<svelte:fragment slot="aside">
<FormList>
<InputText
id="subject"
label="Subject"
disabled={message.status != 'draft'}
bind:value={subject}></InputText>
<InputTextarea
id="message"
label="Message"
disabled={message.status != 'draft'}
bind:value={content}></InputTextarea>
<InputSwitch label="HTML mode" id="html" bind:value={html}>
<svelte:fragment slot="description">
Enable the HTML mode if your message contains HTML tags.
</svelte:fragment>
</InputSwitch>
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button {disabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -1,33 +0,0 @@
<script lang="ts">
import { CardGrid, Heading } from '$lib/components';
import { Button, FormList, InputText, InputTextarea } from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
export let message: Models.Message & { data: Record<string, string> };
export let onEdit: () => void = null;
</script>
<CardGrid>
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
<Heading tag="h6" size="7">Preview</Heading>
</div>
<svelte:fragment slot="aside">
<FormList>
<InputText
id="subject"
label="Subject"
disabled={true}
bind:value={message.data.subject}>
</InputText>
<InputTextarea
id="message"
label="Message"
disabled={true}
bind:value={message.data.content}>
</InputTextarea>
<div class="u-flex u-main-end">
<Button secondary disabled={onEdit == null} on:click={onEdit}>Edit message</Button>
</div>
</FormList>
</svelte:fragment>
</CardGrid>
@@ -1,64 +1,77 @@
<script lang="ts">
import { CardGrid, Heading } from '$lib/components';
import { toLocaleDateTime } from '$lib/helpers/date';
import { message } from './store';
import ProviderType from '../providerType.svelte';
import MessageStatusPill from '../messageStatusPill.svelte';
import { MessagingProviderType } from '@appwrite.io/console';
import type { Models } from '@appwrite.io/console';
import { Button } from '$lib/elements/forms';
import FailedModal from '../failedModal.svelte';
import SendModal from './sendModal.svelte';
import ScheduleModal from './scheduleModal.svelte';
import CancelModal from './cancelModal.svelte';
let scheduledAt: string = '';
export let message: Models.Message & { data: Record<string, string> };
export let topics: Models.Topic[];
let showSend = false;
let showSchedule = false;
let showCancel = false;
let showFailed = false;
let errors: string[] = [];
if ($message.status === 'sent') {
scheduledAt = $message.deliveredAt;
} else if ($message.status === 'scheduled') {
scheduledAt = $message.scheduledAt;
}
let providerType = 'Invalid provider type';
switch ($message.providerType) {
case MessagingProviderType.Email:
providerType = 'Email';
break;
case MessagingProviderType.Sms:
providerType = 'SMS';
break;
case MessagingProviderType.Push:
providerType = 'Push';
break;
}
</script>
<CardGrid hideFooter={$message.status != 'failed'}>
<CardGrid hideFooter={['processing', 'sent'].includes(message.status)}>
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16" data-private>
<ProviderType type={$message.providerType} size="l">
<Heading tag="h6" size="7">{providerType}</Heading>
<ProviderType type={message.providerType} size="l">
<Heading tag="h6" size="7">
<ProviderType type={message.providerType} noIcon />
</Heading>
</ProviderType>
</div>
<svelte:fragment slot="aside">
<div class="u-flex u-main-space-between">
<div data-private>
<p class="title">Created: {toLocaleDateTime($message.$createdAt)}</p>
<p class="title">Scheduled at: {toLocaleDateTime(scheduledAt)}</p>
<p class="title">Created: {toLocaleDateTime(message.$createdAt)}</p>
{#if message.scheduledAt}
<p class="title">Scheduled at: {toLocaleDateTime(message.scheduledAt)}</p>
{/if}
{#if message.deliveredAt}
<p class="title">Sent at: {toLocaleDateTime(message.deliveredAt)}</p>
{/if}
</div>
<div class="u-flex u-flex-vertical u-cross-end">
<MessageStatusPill status={$message.status} />
<MessageStatusPill status={message.status} />
</div>
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
secondary
on:click={(e) => {
e.preventDefault();
errors = $message.deliveryErrors;
showFailed = true;
}}>View logs</Button>
{#if message.status === 'draft'}
<div class="u-flex u-gap-16">
<Button text on:click={() => (showSchedule = true)}>Schedule</Button>
<Button secondary on:click={() => (showSend = true)}>Send message</Button>
</div>
{:else if message.status === 'scheduled'}
<div class="u-flex u-gap-16">
<Button text on:click={() => (showCancel = true)}>Cancel scheduling</Button>
<Button secondary on:click={() => (showSchedule = true)}>Reschedule</Button>
</div>
{:else if message.status === 'failed'}
<Button
secondary
on:click={(e) => {
e.preventDefault();
errors = message.deliveryErrors;
showFailed = true;
}}>View logs</Button>
{/if}
</svelte:fragment>
</CardGrid>
<ScheduleModal bind:show={showSchedule} {message} {topics} />
<SendModal bind:show={showSend} {message} {topics} />
<CancelModal bind:show={showCancel} {message} />
<FailedModal bind:show={showFailed} {errors} />
@@ -0,0 +1,180 @@
<script lang="ts">
import { CardGrid, Heading } from '$lib/components';
import {
Button,
Form,
FormItem,
FormItemPart,
FormList,
Helper,
InputText,
InputTextarea,
Label
} from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
import PushPhone from '../pushPhone.svelte';
import { onMount } from 'svelte';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { validateData } from '../wizard/pushFormList.svelte';
export let message: Models.Message & { data: Record<string, string> };
let title = '';
let body = '';
let originalCustomData: [string, string][] = [['', '']];
let customData: [string, string][] = [['', '']];
let dataError = '';
let disabled = true;
onMount(() => {
title = message.data.title;
body = message.data.body;
const dataEntries: [string, string][] = [];
Object.entries(message.data['data'] ?? {}).forEach(([key, value]) => {
dataEntries.push([key, value.toString()]);
});
customData = dataEntries.length ? dataEntries : [['', '']];
originalCustomData = structuredClone(customData);
});
async function update() {
try {
const data = customData.reduce((acc, [key, value]) => {
if (key) {
acc[key] = value;
}
return acc;
}, {});
await sdk.forProject.messaging.updatePush(
message.$id,
undefined,
undefined,
undefined,
title,
body,
data
);
originalCustomData = structuredClone(customData);
await invalidate(Dependencies.MESSAGING_MESSAGE);
addNotification({
message: 'Message has been updated',
type: 'success'
});
trackEvent(Submit.MessagingMessageUpdate);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.MessagingMessageUpdate);
}
}
$: dataError = validateData(customData || []);
$: disabled =
title === message.data.title &&
body === message.data.body &&
originalCustomData.length === customData.length &&
originalCustomData.every(
(ocd, i) =>
ocd.length === customData[i].length && ocd.every((v, j) => v === customData[i][j])
);
</script>
<Form onSubmit={update}>
<CardGrid hideFooter={message.status != 'draft'}>
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
<Heading tag="h6" size="7">Message</Heading>
<div class="u-flex u-margin-block-start-24 u-width-full-line">
<PushPhone {title} {body} />
</div>
</div>
<svelte:fragment slot="aside">
<FormList>
<InputText
id="title"
label="Title"
disabled={message.status != 'draft'}
bind:value={title}></InputText>
<InputTextarea
id="message"
label="Message"
disabled={message.status != 'draft'}
bind:value={body}></InputTextarea>
<form class="form">
<FormItem>
<Label
tooltip="A key/value payload of additional metadata that's hidden from users. Use this to include information to support logic such as redirection and routing."
>Custom data <span class="u-color-text-gray">(Optional)</span></Label>
</FormItem>
<div class=" u-grid u-gap-8">
<ul class="form-list" style="--p-form-list-gap: 1rem">
{#each customData || [] as _, rowIndex}
<FormItem isMultiple>
<InputText
id={`${rowIndex}-key`}
isMultiple
fullWidth
disabled={message.status != 'draft'}
bind:value={customData[rowIndex][0]}
placeholder="Enter key"
label="Key"
showLabel={false} />
<InputText
id={`${rowIndex}-value`}
isMultiple
fullWidth
disabled={message.status != 'draft'}
bind:value={customData[rowIndex][1]}
placeholder="Enter value"
label="Value"
showLabel={false}
required />
<FormItemPart alignEnd>
<Button
text
disabled={message.status != 'draft'}
on:click={() => {
if (customData.length === 1) {
customData = [['', '']];
return;
}
customData = customData.filter(
(_, i) => i !== rowIndex
);
}}>
<span class="icon-x" aria-hidden="true" />
</Button>
</FormItemPart>
</FormItem>
{/each}
</ul>
{#if dataError}
<Helper type="warning">{dataError}</Helper>
{/if}
<Button
noMargin
text
disabled={customData && customData[customData.length - 1][0] === ''}
on:click={() => {
customData = [...customData, ['', '']];
}}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Add data</span>
</Button>
</div>
</form>
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button {disabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -1,33 +0,0 @@
<script lang="ts">
import { CardGrid, Heading } from '$lib/components';
import { Button, FormList, InputText, InputTextarea } from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
import PushPhone from '../pushPhone.svelte';
export let message: Models.Message & { data: Record<string, string> };
export let onEdit: () => void = null;
</script>
<CardGrid>
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
<Heading tag="h6" size="7">Preview</Heading>
<div class="u-flex u-margin-block-start-24 u-width-full-line">
<PushPhone title={message.data.title} body={message.data.body} />
</div>
</div>
<svelte:fragment slot="aside">
<FormList>
<InputText id="title" label="Title" disabled={true} bind:value={message.data.title}>
</InputText>
<InputTextarea
id="message"
label="Message"
disabled={true}
bind:value={message.data.body}>
</InputTextarea>
<div class="u-flex u-main-end">
<Button secondary disabled={onEdit == null} on:click={onEdit}>Edit message</Button>
</div>
</FormList>
</svelte:fragment>
</CardGrid>
@@ -0,0 +1,138 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Modal } from '$lib/components';
import { Button, FormList, InputDate, InputTime, Helper } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { MessagingProviderType, type Models } from '@appwrite.io/console';
import { Dependencies } from '$lib/constants';
import { isSameDay, toLocaleDateISO, toLocaleTimeISO } from '$lib/helpers/date';
export let show = false;
export let message: Models.Message & { data: Record<string, unknown> };
export let topics: Models.Topic[];
let now = new Date();
let minDate: string;
// Use Sweden's locale (sv) since it matches ISO format
let date = message.scheduledAt == null ? null : toLocaleDateISO(message.scheduledAt);
let time = message.scheduledAt == null ? null : toLocaleTimeISO(message.scheduledAt);
let dateTime: Date;
let totalTargets = message.targets?.length ?? 0;
const formatOptions: Intl.DateTimeFormatOptions = {
month: 'long',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
hourCycle: 'h23',
timeZoneName: 'longGeneric'
};
for (const topic of topics) {
if (message.providerType == MessagingProviderType.Push) {
totalTargets = totalTargets + topic.pushTotal;
} else if (message.providerType == MessagingProviderType.Email) {
totalTargets = totalTargets + topic.emailTotal;
} else if (message.providerType == MessagingProviderType.Sms) {
totalTargets = totalTargets + topic.smsTotal;
}
}
const update = async () => {
try {
if (message.providerType == MessagingProviderType.Email) {
await sdk.forProject.messaging.updateEmail(
message.$id,
undefined,
undefined,
undefined,
undefined,
undefined,
false,
undefined,
undefined,
undefined,
dateTime.toISOString()
);
} else if (message.providerType == MessagingProviderType.Sms) {
await sdk.forProject.messaging.updateSms(
message.$id,
undefined,
undefined,
undefined,
undefined,
false,
dateTime.toISOString()
);
} else if (message.providerType == MessagingProviderType.Push) {
await sdk.forProject.messaging.updatePush(
message.$id,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
false,
dateTime.toISOString()
);
}
await invalidate(Dependencies.MESSAGING_MESSAGE);
addNotification({
message: `The message has been scheduled and will be sent to an estimated ${totalTargets} targets.`,
type: 'success'
});
trackEvent(Submit.MessagingMessageUpdate, {
providerType: message.providerType,
status
});
show = false;
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.MessagingMessageUpdate);
}
};
$: minDate = toLocaleDateISO(now.getTime());
$: minTime = isSameDay(new Date(date), new Date(minDate))
? toLocaleTimeISO(now.getTime())
: '00:00';
$: dateTime = new Date(`${date}T${time}`);
</script>
<Modal title="Schedule message" bind:show onSubmit={update} headerDivider={false} size="big">
<div>
<FormList>
<div
class="u-grid u-gap-16"
style="grid-auto-rows: 1fr; grid-template-columns: 1fr 1fr;">
<InputDate id="date" label="Date" required={true} min={minDate} bind:value={date} />
<InputTime id="time" label="Time" required={true} min={minTime} bind:value={time} />
</div>
</FormList>
<Helper type="neutral">
{#if !dateTime || isNaN(dateTime.getTime())}
The message will be sent later
{:else}
The message will be sent at {dateTime.toLocaleString('en', formatOptions)}
{/if}
</Helper>
</div>
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button submit>Schedule</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,100 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { MessagingProviderType, type Models } from '@appwrite.io/console';
import { Dependencies } from '$lib/constants';
export let show = false;
export let message: Models.Message & { data: Record<string, unknown> };
export let topics: Models.Topic[];
let totalTargets = message.targets?.length ?? 0;
for (const topic of topics) {
if (message.providerType == MessagingProviderType.Push) {
totalTargets = totalTargets + topic.pushTotal;
} else if (message.providerType == MessagingProviderType.Email) {
totalTargets = totalTargets + topic.emailTotal;
} else if (message.providerType == MessagingProviderType.Sms) {
totalTargets = totalTargets + topic.smsTotal;
}
}
const update = async () => {
try {
if (message.providerType == MessagingProviderType.Email) {
await sdk.forProject.messaging.updateEmail(
message.$id,
undefined,
undefined,
undefined,
undefined,
undefined,
false
);
} else if (message.providerType == MessagingProviderType.Sms) {
await sdk.forProject.messaging.updateSms(
message.$id,
undefined,
undefined,
undefined,
undefined,
false
);
} else if (message.providerType == MessagingProviderType.Push) {
await sdk.forProject.messaging.updatePush(
message.$id,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
false
);
}
await invalidate(Dependencies.MESSAGING_MESSAGE);
addNotification({
message: `The message has been sent to an estimated ${totalTargets} targets.`,
type: 'success'
});
trackEvent(Submit.MessagingMessageUpdate, {
providerType: message.providerType,
status
});
show = false;
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.MessagingMessageUpdate);
}
};
</script>
<Modal title="Send message" bind:show onSubmit={update} headerDivider={false} size="small">
<div class="u-flex-vertical u-gap-16">
<p data-private>
You are about to send a message to an estimated <span class="u-bold"
>{totalTargets}</span> targets. Would you like to proceed?
</p>
<p class="u-bold">This action is irreversible.</p>
</div>
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button submit>Send</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,68 @@
<script lang="ts">
import { CardGrid, Heading } from '$lib/components';
import { Button, Form, FormList, InputTextarea } from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
import SMSPhone from '../smsPhone.svelte';
import { onMount } from 'svelte';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
export let message: Models.Message & { data: Record<string, string> };
let content = '';
let disabled = true;
onMount(() => {
content = message.data.content;
});
async function update() {
try {
await sdk.forProject.messaging.updateSms(
message.$id,
undefined,
undefined,
undefined,
content
);
await invalidate(Dependencies.MESSAGING_MESSAGE);
addNotification({
message: 'Message has been updated',
type: 'success'
});
trackEvent(Submit.MessagingMessageUpdate);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.MessagingMessageUpdate);
}
}
$: disabled = content === message.data.content;
</script>
<Form onSubmit={update}>
<CardGrid hideFooter={message.status != 'draft'}>
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
<Heading tag="h6" size="7">Message</Heading>
<SMSPhone {content} />
</div>
<svelte:fragment slot="aside">
<FormList>
<InputTextarea
id="message"
label="Message"
disabled={message.status != 'draft'}
bind:value={content}></InputTextarea>
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button {disabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -1,29 +0,0 @@
<script lang="ts">
import { CardGrid, Heading } from '$lib/components';
import { Button, FormList, InputTextarea } from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
import SMSPhone from '../smsPhone.svelte';
export let message: Models.Message & { data: Record<string, string> };
export let onEdit: () => void = null;
</script>
<CardGrid>
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
<Heading tag="h6" size="7">Preview</Heading>
<SMSPhone content={message.data.content} />
</div>
<svelte:fragment slot="aside">
<FormList>
<InputTextarea
id="message"
label="Message"
disabled={true}
bind:value={message.data.content}>
</InputTextarea>
<div class="u-flex u-main-end">
<Button secondary disabled={onEdit == null} on:click={onEdit}>Edit message</Button>
</div>
</FormList>
</svelte:fragment>
</CardGrid>
@@ -1,62 +0,0 @@
<script lang="ts">
import { MessagingProviderType, type Models } from '@appwrite.io/console';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRow
} from '$lib/elements/table';
import { CardGrid, Heading, Empty, PaginationInline, Id } from '$lib/components';
export let targets: Models.Target[];
export let usersById: Record<string, Models.User<Models.Preferences>>;
let offset = 0;
const limit = 10;
</script>
<CardGrid>
<Heading tag="h6" size="7" id="variables">Targets</Heading>
<svelte:fragment slot="aside">
{@const sum = targets.length}
{#if sum}
<div class="u-flex u-flex-vertical u-gap-24">
<Table noMargin noStyles>
<TableHeader>
<TableCellHead>ID</TableCellHead>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Identifier</TableCellHead>
</TableHeader>
<TableBody>
{#each targets.slice(offset, offset + limit) as target (target.$id)}
<TableRow>
<TableCell title="id">
<Id value={target.$id}>{target.$id}</Id>
</TableCell>
<TableCellText title="name">
{usersById[target.userId]?.name || 'Unknown'}
</TableCellText>
<TableCellText title="subscribers">
{target.providerType === MessagingProviderType.Push
? target.name
: target.identifier}
</TableCellText>
</TableRow>
{/each}
</TableBody>
</Table>
<div class="u-flex u-main-space-between">
<p class="text">Total targets: {sum}</p>
<PaginationInline {sum} {limit} bind:offset />
</div>
</div>
{:else}
<Empty>Edit the message to add a Target</Empty>
{/if}
</svelte:fragment>
</CardGrid>
@@ -1,59 +0,0 @@
<script lang="ts">
import type { Models } from '@appwrite.io/console';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRow
} from '$lib/elements/table';
import { CardGrid, Heading, Empty, PaginationInline, Id } from '$lib/components';
export let topics: Models.Topic[];
let offset = 0;
const limit = 10;
</script>
<CardGrid>
<Heading tag="h6" size="7" id="variables">Topics</Heading>
<svelte:fragment slot="aside">
{@const sum = topics.length}
{#if sum}
<div class="u-flex u-flex-vertical u-gap-24">
<Table noMargin noStyles>
<TableHeader>
<TableCellHead>ID</TableCellHead>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Subscribers</TableCellHead>
</TableHeader>
<TableBody>
{#each topics.slice(offset, offset + limit) as topic (topic.$id)}
<TableRow>
<TableCell title="id">
<Id value={topic.$id}>{topic.$id}</Id>
</TableCell>
<TableCellText title="name">
{topic.name}
</TableCellText>
<TableCellText title="subscribers">
{topic.smsTotal + topic.emailTotal + topic.pushTotal}
</TableCellText>
</TableRow>
{/each}
</TableBody>
</Table>
<div class="u-flex u-main-space-between">
<p class="text">Total topics: {sum}</p>
<PaginationInline {sum} {limit} bind:offset />
</div>
</div>
{:else}
<Empty>Edit the message to add a Topic</Empty>
{/if}
</svelte:fragment>
</CardGrid>
@@ -0,0 +1,208 @@
<script lang="ts">
import { MessagingProviderType, type Models } from '@appwrite.io/console';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableHeader,
TableRow
} from '$lib/elements/table';
import { CardGrid, Heading, Empty, PaginationInline, EmptySearch } from '$lib/components';
import { onMount } from 'svelte';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import { symmetricDifference } from '$lib/helpers/array';
import { addNotification } from '$lib/stores/notifications';
import { Button, Form } from '$lib/elements/forms';
import UserTargetsModal from '../userTargetsModal.svelte';
import { isValueOfStringEnum } from '$lib/helpers/types';
export let message: Models.Message & { data: Record<string, unknown> };
export let selectedTargetsById: Record<string, Models.Target>;
let providerType: MessagingProviderType;
let offset = 0;
const limit = 10;
let showTargets = false;
let targetsById: Record<string, Models.Target>;
let targetIds: string[] = [];
let targets: Models.Target[] = [];
let disabled = true;
if (isValueOfStringEnum(MessagingProviderType, message.providerType)) {
providerType = message.providerType;
}
onMount(() => {
targetsById = { ...selectedTargetsById };
});
function removeTarget(targetId: string) {
const { [targetId]: _, ...rest } = targetsById;
targetsById = rest;
}
async function update() {
try {
if (message.providerType == MessagingProviderType.Email) {
await sdk.forProject.messaging.updateEmail(
message.$id,
undefined,
undefined,
targetIds
);
} else if (message.providerType == MessagingProviderType.Sms) {
await sdk.forProject.messaging.updateSms(
message.$id,
undefined,
undefined,
targetIds
);
} else if (message.providerType == MessagingProviderType.Push) {
await sdk.forProject.messaging.updatePush(
message.$id,
undefined,
undefined,
targetIds
);
}
await invalidate(Dependencies.MESSAGING_MESSAGE);
addNotification({
message: 'Targets have been updated',
type: 'success'
});
trackEvent(Submit.MessagingMessageUpdate);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.MessagingMessageUpdate);
}
}
$: {
targetIds = [];
targets = [];
for (const targetId in targetsById) {
targetIds.push(targetId);
targets.push(targetsById[targetId]);
}
}
$: disabled = symmetricDifference(targetIds, Object.keys(selectedTargetsById)).length === 0;
</script>
<Form onSubmit={update}>
<CardGrid hideFooter={message.status != 'draft'}>
<Heading tag="h6" size="7" id="variables">Targets</Heading>
<svelte:fragment slot="aside">
{@const sum = targetIds.length}
{#if sum}
<div class="u-flex u-cross-center u-main-space-between">
<div>
<span class="eyebrow-heading-3">Target</span>
</div>
{#if message.status == 'draft'}
<Button
text
noMargin
on:click={() => {
showTargets = true;
}}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Add</span>
</Button>
{/if}
</div>
<div class="u-flex u-flex-vertical u-gap-24">
<Table noMargin noStyles>
<TableHeader>
<TableCellHead style="padding: 0" />
<TableCellHead width={40} style="padding: 0" />
</TableHeader>
<TableBody>
<TableRow />
{#each targets.slice(offset, offset + limit) as target (target.$id)}
<TableRow>
<TableCell title="Target">
<div class="u-flex u-cross-center">
<span class="title">
<span class="u-line-height-1-5">
<span class="body-text-2" data-private>
{#if target.providerType === MessagingProviderType.Push}
{target.name}
{:else}
{target.identifier}
{/if}
</span>
</span></span>
</div>
</TableCell>
<TableCell title="Remove">
{#if message.status === 'draft'}
<div
class="u-flex u-main-end"
style="--p-button-size: 1.25rem">
<Button
text
class="is-only-icon"
ariaLabel="delete"
disabled={message.status != 'draft'}
on:click={() => removeTarget(target.$id)}>
<span
class="icon-x u-font-size-20"
aria-hidden="true" />
</Button>
</div>
{/if}
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
<div class="u-flex u-main-space-between">
<p class="text">Total targets: {sum}</p>
<PaginationInline {sum} {limit} bind:offset />
</div>
</div>
{:else if message.status == 'draft'}
<Empty on:click={() => (showTargets = true)}>Add a target</Empty>
{:else}
<EmptySearch hidePagination>
<div class="u-text-center">
No targets have been selected.
<p>
Need a hand? Check out our <Button
link
external
href="https://appwrite.io/docs/products/messaging/targets"
text>
documentation</Button
>.
</p>
</div>
</EmptySearch>
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<Button {disabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<UserTargetsModal
title="Select targets"
{providerType}
bind:show={showTargets}
{targetsById}
on:update={(e) => {
showTargets = false;
targetsById = e.detail;
}}>
<svelte:fragment slot="description"
>Select existing targets to which you want to send this message.</svelte:fragment>
</UserTargetsModal>
@@ -0,0 +1,189 @@
<script lang="ts">
import { type Models, MessagingProviderType } from '@appwrite.io/console';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableHeader,
TableRow
} from '$lib/elements/table';
import { CardGrid, Heading, Empty, PaginationInline, EmptySearch } from '$lib/components';
import TopicsModal from '../topicsModal.svelte';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import { symmetricDifference } from '$lib/helpers/array';
import { Form, Button } from '$lib/elements/forms';
import { onMount } from 'svelte';
import { getTotal } from '../wizard/store';
import { isValueOfStringEnum } from '$lib/helpers/types';
export let message: Models.Message;
export let selectedTopicsById: Record<string, Models.Topic>;
let providerType: MessagingProviderType;
let offset = 0;
const limit = 10;
let showTopics = false;
let topicsById: Record<string, Models.Topic> = {};
let topicIds: string[] = [];
let topics: Models.Topic[] = [];
let disabled = true;
if (isValueOfStringEnum(MessagingProviderType, message.providerType)) {
providerType = message.providerType;
}
onMount(() => {
topicsById = { ...selectedTopicsById };
});
function removeTopic(topicId: string) {
const { [topicId]: _, ...rest } = topicsById;
topicsById = rest;
}
async function update() {
try {
if (message.providerType == MessagingProviderType.Email) {
await sdk.forProject.messaging.updateEmail(message.$id, topicIds);
} else if (message.providerType == MessagingProviderType.Sms) {
await sdk.forProject.messaging.updateSms(message.$id, topicIds);
} else if (message.providerType == MessagingProviderType.Push) {
await sdk.forProject.messaging.updatePush(message.$id, topicIds);
}
await invalidate(Dependencies.MESSAGING_MESSAGE);
addNotification({
message: 'Topics have been updated',
type: 'success'
});
trackEvent(Submit.MessagingMessageUpdate);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.MessagingMessageUpdate);
}
}
$: {
topicIds = [];
topics = [];
for (const topicId in topicsById) {
topicIds.push(topicId);
topics.push(topicsById[topicId]);
}
}
$: disabled = symmetricDifference(topicIds, Object.keys(selectedTopicsById)).length === 0;
</script>
<Form onSubmit={update}>
<CardGrid hideFooter={message.status != 'draft'}>
<Heading tag="h6" size="7" id="variables">Topics</Heading>
<svelte:fragment slot="aside">
{@const sum = topicIds.length}
{#if sum}
<div class="u-flex u-cross-center u-main-space-between">
<div>
<span class="eyebrow-heading-3">Topic</span>
</div>
{#if message.status == 'draft'}
<Button
text
noMargin
on:click={() => {
showTopics = true;
}}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Add</span>
</Button>
{/if}
</div>
<div class="u-flex u-flex-vertical u-gap-24">
<Table noMargin noStyles>
<TableHeader>
<TableCellHead style="padding: 0" />
<TableCellHead width={40} style="padding: 0" />
</TableHeader>
<TableBody>
{#each topics.slice(offset, offset + limit) as topic (topic.$id)}
<TableRow>
<TableCell title="Topic">
<div class="u-flex u-cross-center">
<span class="title">
<span class="u-line-height-1-5">
<span class="body-text-2 u-bold" data-private>
{topic.name}
</span>
<span class="collapsible-button-optional">
({getTotal(topic)} targets)
</span>
</span>
</span>
</div>
</TableCell>
<TableCell title="Remove" width={40}>
{#if message.status === 'draft'}
<div
class="u-flex u-main-end"
style="--p-button-size: 1.25rem">
<Button
text
class="is-only-icon"
ariaLabel="delete"
disabled={message.status != 'draft'}
on:click={() => removeTopic(topic.$id)}>
<span
class="icon-x u-font-size-20"
aria-hidden="true" />
</Button>
</div>
{/if}
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
<div class="u-flex u-main-space-between">
<p class="text">Total topics: {sum}</p>
<PaginationInline {sum} {limit} bind:offset />
</div>
</div>
{:else if message.status == 'draft'}
<Empty on:click={() => (showTopics = true)}>Add a topic</Empty>
{:else}
<EmptySearch hidePagination>
<div class="u-text-center">
No topics have been selected.
<p>
Need a hand? Check out our <Button
link
external
href="https://appwrite.io/docs/products/messaging/topics"
text>
documentation</Button
>.
</p>
</div>
</EmptySearch>
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<Button {disabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
<TopicsModal
{providerType}
bind:show={showTopics}
{topicsById}
on:update={(e) => {
showTopics = false;
topicsById = e.detail;
}} />
@@ -1,5 +1,5 @@
<script lang="ts">
import { messageParams, providerType, operation } from './store';
import { messageParams, providerType } from './store';
import {
Button,
FormList,
@@ -10,7 +10,7 @@
InputTextarea
} from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import { CustomId, Modal } from '$lib/components';
import { Modal } from '$lib/components';
import { user } from '$lib/stores/user';
import { clickOnEnter } from '$lib/helpers/a11y';
import { ID, MessagingProviderType } from '@appwrite.io/console';
@@ -105,20 +105,12 @@
Enable the HTML mode if your message contains HTML tags.
</svelte:fragment>
</InputSwitch>
{#if $operation === 'create'}
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}
><span class="icon-pencil" aria-hidden="true" /><span class="text">
Message ID
</span></Pill>
</div>
{:else}
<CustomId
bind:show={showCustomId}
name="Message"
bind:id={$messageParams[$providerType].messageId}
autofocus={false} />
{/if}
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}
><span class="icon-pencil" aria-hidden="true" /><span class="text">
Message ID
</span></Pill>
</div>
{/if}
</FormList>
@@ -24,7 +24,7 @@
</script>
<script lang="ts">
import { messageParams, providerType, operation } from './store';
import { messageParams, providerType } from './store';
import {
Button,
FormItem,
@@ -38,7 +38,7 @@
Label
} from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import { CustomId, Modal } from '$lib/components';
import { Modal } from '$lib/components';
import { user } from '$lib/stores/user';
import { clickOnEnter } from '$lib/helpers/a11y';
import { ID, MessagingProviderType } from '@appwrite.io/console';
@@ -214,21 +214,13 @@
</Button>
</div>
</form>
{#if $operation === 'create'}
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}
><span class="icon-pencil" aria-hidden="true" /><span class="text">
Message ID
</span></Pill>
</div>
{:else}
<CustomId
bind:show={showCustomId}
name="Message"
bind:id={$messageParams[$providerType].messageId}
autofocus={false} />
{/if}
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}
><span class="icon-pencil" aria-hidden="true" /><span class="text">
Message ID
</span></Pill>
</div>
{/if}
</FormList>
<PushPhone
@@ -1,8 +1,8 @@
<script lang="ts">
import { messageParams, providerType, operation } from './store';
import { messageParams, providerType } from './store';
import { Button, FormList, InputEmail, InputRadio, InputTextarea } from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import { CustomId, Modal } from '$lib/components';
import { Modal } from '$lib/components';
import { user } from '$lib/stores/user';
import { clickOnEnter } from '$lib/helpers/a11y';
import { ID, MessagingProviderType } from '@appwrite.io/console';
@@ -83,21 +83,13 @@
</svelte:fragment>
</Modal>
</div>
{#if $operation === 'create'}
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}
><span class="icon-pencil" aria-hidden="true" /><span class="text">
Message ID
</span></Pill>
</div>
{:else}
<CustomId
bind:show={showCustomId}
name="Message"
bind:id={$messageParams[$providerType].messageId}
autofocus={false} />
{/if}
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}
><span class="icon-pencil" aria-hidden="true" /><span class="text">
Message ID
</span></Pill>
</div>
{/if}
</FormList>
<SMSPhone content={$messageParams[$providerType]['content']} classes="is-only-desktop" />
@@ -4,10 +4,10 @@
import { WizardStep } from '$lib/layout';
import { MessagingProviderType } from '@appwrite.io/console';
import { messageParams, providerType } from './store';
import { isSameDay, toLocaleDateISO, toLocaleTimeISO } from '$lib/helpers/date';
let when: 'now' | 'later' = 'now';
let now = new Date();
let timeZoneOffset: number;
let minDate: string;
let date: string;
let time: string;
@@ -40,28 +40,23 @@
timeZoneName: 'longGeneric'
};
async function beforeSubmit() {
if (when === 'later') {
$messageParams[$providerType].scheduledAt = dateTime.toISOString();
}
}
$: if (when === 'now') {
date = time = '';
}
$: if (when === 'later') {
now = new Date();
}
$: timeZoneOffset = now ? now.getTimezoneOffset() * 60 * 1000 : 0;
$: minDate = new Date(now.getTime() - timeZoneOffset).toISOString().split('T')[0];
$: minTime =
date === minDate
? new Date(now.getTime() - timeZoneOffset).toISOString().split('T')[1].substring(0, 5)
: '00:00';
$: minDate = toLocaleDateISO(now.getTime());
$: minTime = isSameDay(new Date(date), new Date(minDate))
? toLocaleTimeISO(now.getTime())
: '00:00';
$: dateTime = new Date(`${date}T${time}`);
$: if (!isNaN(dateTime.getTime())) {
$messageParams[$providerType].scheduledAt = dateTime.toISOString();
}
</script>
<WizardStep {beforeSubmit}>
<WizardStep>
<svelte:fragment slot="title">Schedule</svelte:fragment>
<svelte:fragment slot="subtitle"
>Schedule the time you want to deliver this message. Learn more in our <Button
@@ -32,7 +32,6 @@ export type PushMessageParams = MessageParams & {
badge?: string;
};
export const operation = writable<'create' | 'update'>('create');
export const providerType = writable<MessagingProviderType>(null);
export const targetsById = writable<Record<string, Models.Target>>({});
export const messageParams = writable<{
+39 -3
View File
@@ -4,13 +4,16 @@ import {
toLocaleDateTime,
isSameDay,
isValidDate,
diffDays
diffDays,
toLocaleDateISO,
toLocaleTimeISO
} from '$lib/helpers/date';
describe('local date', () => {
[
['2022-11-15 08:26:28', 'Nov 15, 2022'],
['2022-11-15 00:26:28', 'Nov 15, 2022']
['2022-11-15 00:26:28', 'Nov 15, 2022'],
['2022-11-15 00:26:28Z', 'Nov 14, 2022']
].forEach(([value, expected]) => {
it(value, () => {
expect(toLocaleDate(value)).toBe(expected);
@@ -25,7 +28,8 @@ describe('local date', () => {
describe('local date time', () => {
[
['2022-11-15 08:26:28', 'Nov 15, 2022, 08:26'],
['2022-11-15 00:26:28', 'Nov 15, 2022, 00:26']
['2022-11-15 00:26:28', 'Nov 15, 2022, 00:26'],
['2022-11-15 00:26:28Z', 'Nov 14, 2022, 19:26']
].forEach(([value, expected]) => {
it(value, () => {
expect(toLocaleDateTime(value)).toBe(expected);
@@ -37,6 +41,38 @@ describe('local date time', () => {
});
});
describe('local date ISO', () => {
[
['2022-11-15 20:26:28Z', '2022-11-15'],
['2022-11-15 08:26:28Z', '2022-11-15'],
['2022-11-16 00:26:28Z', '2022-11-15']
].forEach(([value, expected]) => {
it(value, () => {
expect(toLocaleDateISO(value)).toBe(expected);
});
});
it('invalid date', () => {
expect(toLocaleDateISO('')).toBe('n/a');
});
});
describe('local time ISO', () => {
[
['2022-11-15 20:26:28Z', '15:26:28'],
['2022-11-15 08:26:28Z', '03:26:28'],
['2022-11-16 00:26:28Z', '19:26:28']
].forEach(([value, expected]) => {
it(value, () => {
expect(toLocaleTimeISO(value)).toBe(expected);
});
});
it('invalid date', () => {
expect(toLocaleTimeISO('')).toBe('n/a');
});
});
describe('is same day', () => {
const entries: Array<[string, string, boolean]> = [
['2022-11-15 08:26:28', '2022-11-15 08:26:28', true],