diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index aaac701734..8ebe53c4f9 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -18,8 +18,8 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
- # - name: Audit dependencies
- # run: npm audit --audit-level low
+ - name: Audit dependencies
+ run: npm audit --audit-level low
- name: Install dependencies
run: npm ci
- name: Svelte Diagnostics
diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts
index 8a15993fcf..39551514b2 100644
--- a/src/lib/actions/analytics.ts
+++ b/src/lib/actions/analytics.ts
@@ -287,5 +287,16 @@ export enum Submit {
SmsResetTemplate = 'submit_sms_reset_template',
SmsUpdateInviteTemplate = 'submit_sms_update_invite_template',
SmsUpdateLoginTemplate = 'submit_sms_update_login_template',
- SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template'
+ SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template',
+ MessagingProviderCreate = 'submit_messaging_provider_create',
+ MessagingProviderDelete = 'submit_messaging_provider_delete',
+ MessagingProviderUpdate = 'submit_messaging_provider_update',
+ MessagingMessageCreate = 'submit_messaging_message_create',
+ MessagingMessageDelete = 'submit_messaging_message_delete',
+ MessagingTopicCreate = 'submit_messaging_topic_create',
+ MessagingTopicDelete = 'submit_messaging_topic_delete',
+ MessagingTopicUpdateName = 'submit_messaging_topic_update_name',
+ MessagingTopicUpdateDescription = 'submit_messaging_topic_update_description',
+ MessagingTopicSubscriberAdd = 'submit_messaging_topic_subscriber_add',
+ MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete'
}
diff --git a/src/lib/commandCenter/commands.ts b/src/lib/commandCenter/commands.ts
index f12bbe2ed8..470a610129 100644
--- a/src/lib/commandCenter/commands.ts
+++ b/src/lib/commandCenter/commands.ts
@@ -17,6 +17,7 @@ const groups = [
'platforms',
'databases',
'functions',
+ 'messaging',
'storage',
'domains',
'webhooks',
diff --git a/src/lib/components/drop.svelte b/src/lib/components/drop.svelte
index d6c899df21..f61ff6a246 100644
--- a/src/lib/components/drop.svelte
+++ b/src/lib/components/drop.svelte
@@ -13,6 +13,7 @@
export let noStyle = false;
export let fullWidth = false;
export let fixed = false;
+ export let display = 'block';
const dispatch = createEventDispatcher<{
blur: undefined;
@@ -100,7 +101,11 @@
-
+
diff --git a/src/lib/components/emptyFilter.svelte b/src/lib/components/emptyFilter.svelte
new file mode 100644
index 0000000000..5515fbdd10
--- /dev/null
+++ b/src/lib/components/emptyFilter.svelte
@@ -0,0 +1,26 @@
+
+
+
+
+
+
Sorry, we couldn't find any {resource}.
+
There are no {resource} that match your filters.
+
+
+ {
+ queries.clearAll();
+ queries.apply();
+ }}>
+ Clear filters
+
+
+
+
diff --git a/src/lib/components/filters/content.svelte b/src/lib/components/filters/content.svelte
index 6da14dcc69..1f6717663c 100644
--- a/src/lib/components/filters/content.svelte
+++ b/src/lib/components/filters/content.svelte
@@ -118,10 +118,12 @@
({
- label: c.title,
- value: c.id
- }))}
+ options={$columns
+ .filter((c) => c.filter !== false)
+ .map((c) => ({
+ label: c.title,
+ value: c.id
+ }))}
placeholder="Select column"
bind:value={columnId} />
import { tooltip } from '$lib/actions/tooltip';
+ import { app } from '$lib/stores/app';
+ import { base } from '$app/paths';
export let name: string;
export let group: string;
@@ -7,6 +9,7 @@
export let disabled = false;
export let padding = 1;
export let icon: string = null;
+ export let imageIcon: string = null;
export let fullHeight = true;
export let borderRadius: 'xsmall' | 'small' | 'medium' | 'large' = 'small';
export let backgroundColor: string = null;
@@ -58,5 +61,12 @@
{/if}
{/if}
+ {#if imageIcon}
+
+ {/if}
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 52d2b999bd..da342ce92c 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -46,7 +46,13 @@ export enum Dependencies {
MIGRATIONS = 'dependency:migrations',
COLLECTIONS = 'dependency:collections',
RUNTIMES = 'dependency:runtimes',
- CONSOLE_VARIABLES = 'dependency:console_variables'
+ CONSOLE_VARIABLES = 'dependency:console_variables',
+ MESSAGING_PROVIDERS = 'dependency:messaging_providers',
+ MESSAGING_PROVIDER = 'dependency:messaging_provider',
+ MESSAGING_MESSAGE = 'dependency:messaging_message',
+ MESSAGING_TOPICS = 'dependency:messaging_topics',
+ MESSAGING_TOPIC = 'dependency:messaging_topic',
+ MESSAGING_TOPIC_SUBSCRIBERS = 'dependency:messaging_topic_subscribers'
}
export const scopes: {
diff --git a/src/lib/elements/forms/index.ts b/src/lib/elements/forms/index.ts
index 22f3688a2d..9858ae0c17 100644
--- a/src/lib/elements/forms/index.ts
+++ b/src/lib/elements/forms/index.ts
@@ -29,3 +29,4 @@ export { default as Label } from './label.svelte';
export { default as InputProjectId } from './inputProjectId.svelte';
export { default as InputDate } from './inputDate.svelte';
export { default as InputDateRange } from './inputDateRange.svelte';
+export { default as InputTime } from './inputTime.svelte';
diff --git a/src/lib/elements/forms/inputCheckbox.svelte b/src/lib/elements/forms/inputCheckbox.svelte
index 4aaf52c6be..0493d3f0b3 100644
--- a/src/lib/elements/forms/inputCheckbox.svelte
+++ b/src/lib/elements/forms/inputCheckbox.svelte
@@ -1,13 +1,10 @@
- {#if label}
-
- {label}
-
- {/if}
-
-
-
-
+
+
+
+
+
+ {#if label}
+
{label}
+ {/if}
+
+
+
{#if error}
{error}
{/if}
diff --git a/src/lib/elements/forms/inputDate.svelte b/src/lib/elements/forms/inputDate.svelte
index a49aa3c4ee..380abe2793 100644
--- a/src/lib/elements/forms/inputDate.svelte
+++ b/src/lib/elements/forms/inputDate.svelte
@@ -10,6 +10,8 @@
export let value = '';
export let required = false;
export let nullable = false;
+ export let min: string | number | undefined = undefined;
+ export let max: string | number | undefined = undefined;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
@@ -65,6 +67,8 @@
{readonly}
{required}
step=".001"
+ {min}
+ {max}
autocomplete={autocomplete ? 'on' : 'off'}
type="date"
class="input-text"
diff --git a/src/lib/elements/forms/inputDomain.svelte b/src/lib/elements/forms/inputDomain.svelte
index 58f0661999..5445b2ccbc 100644
--- a/src/lib/elements/forms/inputDomain.svelte
+++ b/src/lib/elements/forms/inputDomain.svelte
@@ -1,6 +1,7 @@
+
+
+
+ {label}
+
+
+
+
+
+ {#if error}
+ {error}
+ {/if}
+
diff --git a/src/lib/helpers/types.ts b/src/lib/helpers/types.ts
index b6d02e7a17..35e8214c9b 100644
--- a/src/lib/helpers/types.ts
+++ b/src/lib/helpers/types.ts
@@ -19,6 +19,13 @@ export type Column = {
id: string;
title: string;
type: ColumnType;
+ /**
+ * Set to false to hide by default
+ */
show: boolean;
width?: number;
+ /**
+ * Set to false to disable filtering for this column
+ */
+ filter?: boolean;
};
diff --git a/src/routes/console/project-[project]/databases/database-[database]/collection-[collection]/+page.svelte b/src/routes/console/project-[project]/databases/database-[database]/collection-[collection]/+page.svelte
index c622114491..e2948ef64b 100644
--- a/src/routes/console/project-[project]/databases/database-[database]/collection-[collection]/+page.svelte
+++ b/src/routes/console/project-[project]/databases/database-[database]/collection-[collection]/+page.svelte
@@ -1,8 +1,7 @@
+
+
+ Messaging - Appwrite
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/+layout.ts b/src/routes/console/project-[project]/messaging/+layout.ts
new file mode 100644
index 0000000000..7db345058e
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/+layout.ts
@@ -0,0 +1,10 @@
+import Breadcrumbs from './breadcrumbs.svelte';
+import Header from './header.svelte';
+import type { LayoutLoad } from './$types';
+
+export const load: LayoutLoad = async () => {
+ return {
+ header: Header,
+ breadcrumbs: Breadcrumbs
+ };
+};
diff --git a/src/routes/console/project-[project]/messaging/+page.svelte b/src/routes/console/project-[project]/messaging/+page.svelte
new file mode 100644
index 0000000000..35558ebe46
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/+page.svelte
@@ -0,0 +1,246 @@
+
+
+
+
+
+ {#if data.messages.total}
+
+
+ d.$id)} />
+ {#each $columns as column}
+ {#if column.show}
+ {column.title}
+ {/if}
+ {/each}
+
+
+ {#each data.messages.messages as message}
+
+
+
+ {#each $columns as column (column.id)}
+ {#if column.show}
+ {#if column.id === '$id'}
+ {#key $columns}
+
+ {message.$id}
+
+ {/key}
+ {:else if column.id === 'message'}
+
+ {#if message.providerType === ProviderTypes.Push}
+ {message.data.title}
+ {:else if message.providerType === ProviderTypes.Sms}
+ {message.data.content}
+ {:else if message.providerType === ProviderTypes.Email}
+ {message.data.subject}
+ {:else}
+ Invalid provider
+ {/if}
+
+ {:else if column.id === 'providerType'}
+
+
+
+ {:else if column.id === 'status'}
+
+ {
+ e.preventDefault();
+ errors = message.deliveryErrors;
+ showFailed = true;
+ }} />
+
+ {:else if column.type === 'datetime'}
+
+ {#if !message[column.id]}
+ -
+ {:else}
+ {toLocaleDateTime(message[column.id])}
+ {/if}
+
+ {:else}
+
+ {message[column.id]}
+
+ {/if}
+ {/if}
+ {/each}
+
+ {/each}
+
+
+
+ 0}>
+
+
+
{selected.length}
+
+
+ {selected.length > 1 ? 'messages' : 'message'}
+
+ selected
+
+
+
+
+
(selected = [])}>Cancel
+
+
(showDelete = true)}>
+ Delete
+
+
+
+
+
+
+ {:else if $hasPageQueries}
+
+
+ {:else if data.search && data.search != 'empty'}
+
+
+
Sorry, we couldn't find '{data.search}'
+
There are no messages that match your search.
+
+
+
+
+ Documentation
+
+
+ Clear search
+
+
+
+ {:else}
+
+ ($showCreate = true)}>
+
+
+ Create your first message to get started.
+
+
+ Need a hand? Learn more in our documentation.
+
+
+
+
+ Documentation
+
+
+ (showCreateDropdownEmpty = !showCreateDropdownEmpty)}
+ event="create_message">
+ Create message
+
+
+
+
+ {/if}
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/+page.ts b/src/routes/console/project-[project]/messaging/+page.ts
new file mode 100644
index 0000000000..85eb3722cf
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/+page.ts
@@ -0,0 +1,72 @@
+import {
+ View,
+ getLimit,
+ getPage,
+ getQuery,
+ getSearch,
+ getView,
+ pageToOffset
+} from '$lib/helpers/load';
+import { CARD_LIMIT } from '$lib/constants';
+import type { PageLoad } from './$types';
+import { messages as data, providersById } from './store';
+import type { Message } from './store';
+import { Query } from '@appwrite.io/console';
+import { sdk } from '$lib/stores/sdk';
+import { queries, queryParamToMap } from '$lib/components/filters';
+
+export const load: PageLoad = async ({ url, route }) => {
+ const page = getPage(url);
+ const search = getSearch(url);
+ const view = getView(url, route, View.Grid);
+ const limit = getLimit(url, route, CARD_LIMIT);
+ const offset = pageToOffset(page, limit);
+ const query = getQuery(url);
+
+ const parsedQueries = queryParamToMap(query || '[]');
+ queries.set(parsedQueries);
+
+ // TODO: remove when the API is ready with data
+ // This allows us to mock w/ data and when search returns 0 results
+ let messages: { messages: Message[]; total: number } = { messages: [], total: 0 };
+ if (search === 'demo') {
+ messages = data;
+ } else {
+ const params = {
+ queries: [
+ Query.limit(limit),
+ Query.offset(offset),
+ Query.orderDesc(''),
+ ...parsedQueries.values()
+ ]
+ };
+
+ if (search) {
+ params['search'] = search;
+ }
+
+ const response = await sdk.forProject.client.call(
+ 'GET',
+ new URL(sdk.forProject.client.config.endpoint + '/messaging/messages'),
+ {
+ 'X-Appwrite-Project': sdk.forProject.client.config.project,
+ 'content-type': 'application/json',
+ 'X-Appwrite-Mode': 'admin'
+ },
+ params
+ );
+
+ messages = response;
+ }
+
+ return {
+ offset,
+ limit,
+ search,
+ query,
+ page,
+ view,
+ messages,
+ providersById
+ };
+};
diff --git a/src/routes/console/project-[project]/messaging/actions.svelte b/src/routes/console/project-[project]/messaging/actions.svelte
new file mode 100644
index 0000000000..bf2bb5b7bf
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/actions.svelte
@@ -0,0 +1,44 @@
+
+
+
+
+
+ (showTopics = true)}>Select topics
+ (showUserTargets = true)}>Select targets
+
+
+
+ {
+ showTopics = false;
+ dispatch('addTopics', e.detail);
+ }} />
+ {
+ showUserTargets = false;
+ dispatch('addTargets', e.detail);
+ }} />
diff --git a/src/routes/console/project-[project]/messaging/breadcrumbs.svelte b/src/routes/console/project-[project]/messaging/breadcrumbs.svelte
new file mode 100644
index 0000000000..9e28bee679
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/breadcrumbs.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/bubble-tail-dark.svg b/src/routes/console/project-[project]/messaging/bubble-tail-dark.svg
new file mode 100644
index 0000000000..b5e7c0ba72
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/bubble-tail-dark.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/bubble-tail-light.svg b/src/routes/console/project-[project]/messaging/bubble-tail-light.svg
new file mode 100644
index 0000000000..9dcbbe34f4
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/bubble-tail-light.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/create.svelte b/src/routes/console/project-[project]/messaging/create.svelte
new file mode 100644
index 0000000000..52e96a0292
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/create.svelte
@@ -0,0 +1,114 @@
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/createMessageDropdown.svelte b/src/routes/console/project-[project]/messaging/createMessageDropdown.svelte
new file mode 100644
index 0000000000..7a596496af
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/createMessageDropdown.svelte
@@ -0,0 +1,69 @@
+
+
+
+
+ (showCreateDropdown = !showCreateDropdown)} event="create_message">
+
+ Create message
+
+
+
+ {#each Object.entries(providers) as [type, option]}
+ {
+ if (
+ type !== ProviderTypes.Email &&
+ type !== ProviderTypes.Sms &&
+ type !== ProviderTypes.Push
+ )
+ return;
+ $providerType = type;
+ $topicsById = {};
+ $targetsById = {};
+ const common = {
+ topics: [],
+ users: [],
+ targets: []
+ };
+ switch (type) {
+ case ProviderTypes.Email:
+ $messageParams[$providerType] = {
+ ...common,
+ subject: '',
+ content: ''
+ };
+ break;
+ case ProviderTypes.Sms:
+ $messageParams[$providerType] = {
+ ...common,
+ content: ''
+ };
+ break;
+ case ProviderTypes.Push:
+ $messageParams[$providerType] = {
+ ...common,
+ title: '',
+ body: ''
+ };
+ break;
+ }
+ showCreateDropdown = false;
+ wizard.start(Create);
+ }}>
+ {option.name}
+
+ {/each}
+
+
diff --git a/src/routes/console/project-[project]/messaging/failedModal.svelte b/src/routes/console/project-[project]/messaging/failedModal.svelte
new file mode 100644
index 0000000000..6ee183ffd1
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/failedModal.svelte
@@ -0,0 +1,21 @@
+
+
+
+
+
Some messages failed to send.
+
+
+
+
+
+
+ (show = false)}>Close
+
+
diff --git a/src/routes/console/project-[project]/messaging/header.svelte b/src/routes/console/project-[project]/messaging/header.svelte
new file mode 100644
index 0000000000..9534915abb
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/header.svelte
@@ -0,0 +1,44 @@
+
+
+
+
+ Messaging
+
+
+ {#each tabs as tab}
+
+ {tab.title}
+
+ {/each}
+
+
diff --git a/src/routes/console/project-[project]/messaging/message-[message]/+layout.svelte b/src/routes/console/project-[project]/messaging/message-[message]/+layout.svelte
new file mode 100644
index 0000000000..9d2f6426ef
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/message-[message]/+layout.svelte
@@ -0,0 +1,5 @@
+
+ Message - Appwrite
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/message-[message]/+layout.ts b/src/routes/console/project-[project]/messaging/message-[message]/+layout.ts
new file mode 100644
index 0000000000..21e6f6aad6
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/message-[message]/+layout.ts
@@ -0,0 +1,33 @@
+import type { LayoutLoad } from './$types';
+import Breadcrumbs from './breadcrumbs.svelte';
+import Header from './header.svelte';
+import { sdk } from '$lib/stores/sdk';
+import { Dependencies } from '$lib/constants';
+import { error } from '@sveltejs/kit';
+import type { Message } from '../store';
+
+export const load: LayoutLoad = async ({ params, depends }) => {
+ depends(Dependencies.MESSAGING_MESSAGE);
+
+ try {
+ const response: Message = await sdk.forProject.client.call(
+ 'GET',
+ new URL(
+ `${sdk.forProject.client.config.endpoint}/messaging/messages/${params.message}`
+ ),
+ {
+ 'X-Appwrite-Project': sdk.forProject.client.config.project,
+ 'content-type': 'application/json',
+ 'X-Appwrite-Mode': 'admin'
+ }
+ );
+
+ return {
+ header: Header,
+ breadcrumbs: Breadcrumbs,
+ message: response
+ };
+ } catch (e) {
+ throw error(e.code, e.message);
+ }
+};
diff --git a/src/routes/console/project-[project]/messaging/message-[message]/+page.svelte b/src/routes/console/project-[project]/messaging/message-[message]/+page.svelte
new file mode 100644
index 0000000000..c949eba855
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/message-[message]/+page.svelte
@@ -0,0 +1,22 @@
+
+
+
+
+ {#if $message.providerType === ProviderTypes.Email}
+
+ {:else if $message.providerType === ProviderTypes.Sms}
+
+ {:else if $message.providerType === ProviderTypes.Push}
+
+ {/if}
+
+
diff --git a/src/routes/console/project-[project]/messaging/message-[message]/breadcrumbs.svelte b/src/routes/console/project-[project]/messaging/message-[message]/breadcrumbs.svelte
new file mode 100644
index 0000000000..caf1d7778f
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/message-[message]/breadcrumbs.svelte
@@ -0,0 +1,24 @@
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/message-[message]/delete.svelte b/src/routes/console/project-[project]/messaging/message-[message]/delete.svelte
new file mode 100644
index 0000000000..7d7a2fb777
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/message-[message]/delete.svelte
@@ -0,0 +1,38 @@
+
+
+
+ Delete message
+
+ The message will be permanently deleted, including all data associated with this message.
+ This action is irreversible.
+
+
+
+
+
+ {$message.data.title ??
+ $message.data.subject ??
+ $message.data.content ??
+ 'Message'}
+
+
+
+ Last updated: {toLocaleDateTime($message.$updatedAt)}
+
+
+
+
+
+ (showDelete = true)} event="delete_file">Delete
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/message-[message]/deleteModal.svelte b/src/routes/console/project-[project]/messaging/message-[message]/deleteModal.svelte
new file mode 100644
index 0000000000..41988ae730
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/message-[message]/deleteModal.svelte
@@ -0,0 +1,56 @@
+
+
+
+ Are you sure you want to delete this message?
+
+ (show = false)}>Cancel
+ Delete
+
+
diff --git a/src/routes/console/project-[project]/messaging/message-[message]/emailPreview.svelte b/src/routes/console/project-[project]/messaging/message-[message]/emailPreview.svelte
new file mode 100644
index 0000000000..153a14def8
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/message-[message]/emailPreview.svelte
@@ -0,0 +1,33 @@
+
+
+
+
+ Preview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/message-[message]/header.svelte b/src/routes/console/project-[project]/messaging/message-[message]/header.svelte
new file mode 100644
index 0000000000..f97cc9e213
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/message-[message]/header.svelte
@@ -0,0 +1,17 @@
+
+
+
+
+
+ {$message.data.title ?? $message.data.subject ?? $message.data.content ?? 'Message'}
+
+ {$message.$id}
+
+
diff --git a/src/routes/console/project-[project]/messaging/message-[message]/overview.svelte b/src/routes/console/project-[project]/messaging/message-[message]/overview.svelte
new file mode 100644
index 0000000000..9dfb7add38
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/message-[message]/overview.svelte
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
Created: {toLocaleDateTime($message.$createdAt)}
+
Scheduled at: {toLocaleDateTime(scheduledAt)}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/message-[message]/pushPreview.svelte b/src/routes/console/project-[project]/messaging/message-[message]/pushPreview.svelte
new file mode 100644
index 0000000000..b5695ed3fb
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/message-[message]/pushPreview.svelte
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/message-[message]/smsPreview.svelte b/src/routes/console/project-[project]/messaging/message-[message]/smsPreview.svelte
new file mode 100644
index 0000000000..5648c9bf23
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/message-[message]/smsPreview.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+ Preview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/message-[message]/store.ts b/src/routes/console/project-[project]/messaging/message-[message]/store.ts
new file mode 100644
index 0000000000..97b3c1c5d6
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/message-[message]/store.ts
@@ -0,0 +1,5 @@
+import { derived } from 'svelte/store';
+import { page } from '$app/stores';
+import type { Message } from '../store';
+
+export const message = derived(page, ($page) => $page.data.message as Message);
diff --git a/src/routes/console/project-[project]/messaging/messageStatusPill.svelte b/src/routes/console/project-[project]/messaging/messageStatusPill.svelte
new file mode 100644
index 0000000000..46b309abc7
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/messageStatusPill.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {#if status === 'sent'}
+
+ {:else if status === 'scheduled'}
+
+ {/if}
+
+ {status}
+
+
diff --git a/src/routes/console/project-[project]/messaging/provider.svelte b/src/routes/console/project-[project]/messaging/provider.svelte
new file mode 100644
index 0000000000..5e2da923a0
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/provider.svelte
@@ -0,0 +1,96 @@
+
+
+
+
+{#if icon === ''}
+ Invalid provider
+{:else}
+
+ {#if !noIcon}
+
+
+
+ {/if}
+
+ {displayName}
+
+
+{/if}
diff --git a/src/routes/console/project-[project]/messaging/providerType.svelte b/src/routes/console/project-[project]/messaging/providerType.svelte
new file mode 100644
index 0000000000..d5e43f9f53
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providerType.svelte
@@ -0,0 +1,59 @@
+
+
+
+
+{#if text === ''}
+ Invalid provider type
+{:else}
+
+ {#if !noIcon}
+
+
+
+ {/if}
+
+ {text}
+
+
+{/if}
diff --git a/src/routes/console/project-[project]/messaging/providers/+page.svelte b/src/routes/console/project-[project]/messaging/providers/+page.svelte
new file mode 100644
index 0000000000..83acdb53a9
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/+page.svelte
@@ -0,0 +1,115 @@
+
+
+
+
+ {#if data.providers.total}
+
+
+
+ {:else if $hasPageQueries}
+
+ {:else if data.search && data.search != 'empty'}
+
+
+
Sorry, we couldn't find '{data.search}'
+
There are no providers that match your search.
+
+
+ Clear search
+
+
+ {:else}
+
+
+
+
+ Create your first provider to get started.
+
+
+ Need a hand? Learn more in our documentation.
+
+
+
+
+ Documentation
+
+
+ (showCreateDropdownEmpty = !showCreateDropdownEmpty)}
+ event="create_provider">
+ Create provider
+
+
+
+
+ {/if}
+
diff --git a/src/routes/console/project-[project]/messaging/providers/+page.ts b/src/routes/console/project-[project]/messaging/providers/+page.ts
new file mode 100644
index 0000000000..c8f65c5bea
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/+page.ts
@@ -0,0 +1,77 @@
+import { Query } from '@appwrite.io/console';
+import { sdk } from '$lib/stores/sdk';
+import {
+ View,
+ getLimit,
+ getPage,
+ getQuery,
+ getSearch,
+ getView,
+ pageToOffset
+} from '$lib/helpers/load';
+import { Dependencies, PAGE_LIMIT } from '$lib/constants';
+import { providersById, type Provider } from '../store';
+import { queries, queryParamToMap } from '$lib/components/filters';
+
+const providers = Object.values(providersById);
+
+let data: { providers: Provider[]; total: number } = {
+ providers: [...providers],
+ total: providers.length
+};
+
+export const load = async ({ depends, url, route }) => {
+ depends(Dependencies.MESSAGING_PROVIDERS);
+
+ const page = getPage(url);
+ const search = getSearch(url);
+ const view = getView(url, route, View.Grid);
+ const limit = getLimit(url, route, PAGE_LIMIT);
+ const offset = pageToOffset(page, limit);
+ const query = getQuery(url);
+
+ const parsedQueries = queryParamToMap(query || '[]');
+ queries.set(parsedQueries);
+
+ // TODO: get rid of demo data
+ let providers: { providers: Provider[]; total: number } = { providers: [], total: 0 };
+ if (search == 'demo') {
+ providers = data;
+ } else {
+ const params = {
+ queries: [
+ Query.limit(limit),
+ Query.offset(offset),
+ Query.orderDesc(''),
+ ...parsedQueries.values()
+ ]
+ };
+
+ if (search) {
+ params['search'] = search;
+ }
+
+ const response = await sdk.forProject.client.call(
+ 'GET',
+ new URL(sdk.forProject.client.config.endpoint + '/messaging/providers'),
+ {
+ 'X-Appwrite-Project': sdk.forProject.client.config.project,
+ 'content-type': 'application/json',
+ 'X-Appwrite-Mode': 'admin'
+ },
+ params
+ );
+
+ providers = response;
+ }
+
+ return {
+ offset,
+ limit,
+ search,
+ query,
+ page,
+ view,
+ providers
+ };
+};
diff --git a/src/routes/console/project-[project]/messaging/providers/create.svelte b/src/routes/console/project-[project]/messaging/providers/create.svelte
new file mode 100644
index 0000000000..d828be10c8
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/create.svelte
@@ -0,0 +1,274 @@
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/providers/createProviderDropdown.svelte b/src/routes/console/project-[project]/messaging/providers/createProviderDropdown.svelte
new file mode 100644
index 0000000000..d027d35941
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/createProviderDropdown.svelte
@@ -0,0 +1,49 @@
+
+
+
+
+ (showCreateDropdown = !showCreateDropdown)} event="create_provider">
+
+ Create provider
+
+
+
+ {#each Object.entries(providers) as [type, option]}
+ {
+ if (
+ type !== ProviderTypes.Email &&
+ type !== ProviderTypes.Sms &&
+ type !== ProviderTypes.Push
+ )
+ return;
+ $providerType = type;
+ const p = Object.keys(providers[type].providers).shift();
+ if (p && isValueOfStringEnum(Providers, p)) {
+ $provider = p;
+ }
+ showCreateDropdown = false;
+ wizard.start(Create);
+ }}>
+ {option.name}
+
+ {/each}
+
+
diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.svelte
new file mode 100644
index 0000000000..944197a93a
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.svelte
@@ -0,0 +1,5 @@
+
+ Provider - Appwrite
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.ts b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.ts
new file mode 100644
index 0000000000..3527cd7373
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.ts
@@ -0,0 +1,30 @@
+import type { LayoutLoad } from './$types';
+import Breadcrumbs from './breadcrumbs.svelte';
+import Header from './header.svelte';
+import { sdk } from '$lib/stores/sdk';
+import { Dependencies } from '$lib/constants';
+import { error } from '@sveltejs/kit';
+
+export const load: LayoutLoad = async ({ params, depends }) => {
+ depends(Dependencies.MESSAGING_PROVIDER);
+
+ const response = await sdk.forProject.client.call(
+ 'GET',
+ new URL(sdk.forProject.client.config.endpoint + '/messaging/providers/' + params.provider),
+ {
+ 'X-Appwrite-Project': sdk.forProject.client.config.project,
+ 'content-type': 'application/json',
+ 'X-Appwrite-Mode': 'admin'
+ }
+ );
+
+ try {
+ return {
+ header: Header,
+ breadcrumbs: Breadcrumbs,
+ provider: response
+ };
+ } catch (e) {
+ throw error(e.code, e.message);
+ }
+};
diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+page.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+page.svelte
new file mode 100644
index 0000000000..2262f62ac1
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+page.svelte
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/breadcrumbs.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/breadcrumbs.svelte
new file mode 100644
index 0000000000..5a3f26f90d
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/breadcrumbs.svelte
@@ -0,0 +1,27 @@
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/dangerZone.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/dangerZone.svelte
new file mode 100644
index 0000000000..7befc15c0e
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/dangerZone.svelte
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+ Delete provider
+
+ The provider's instance will be permanently deleted. This action is irreversible.
+
+
+
+ {$provider.name}
+
+
+ Last updated: {toLocaleDateTime($provider.$updatedAt)}
+
+
+
+
+
+ ($showDelete = true)} event="delete_messaging_provider"
+ >Delete
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/deleteProvider.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/deleteProvider.svelte
new file mode 100644
index 0000000000..e32cbf6aae
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/deleteProvider.svelte
@@ -0,0 +1,59 @@
+
+
+
+
+ Are you sure you want to delete {$provider.name} from '{$project.name}'?
+
+
+ (showDelete = false)}>Cancel
+ Delete
+
+
diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/header.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/header.svelte
new file mode 100644
index 0000000000..d78a0f0d43
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/header.svelte
@@ -0,0 +1,17 @@
+
+
+
+
+
+ {$provider?.name ? $provider?.name : '-'}
+
+ {$provider?.$id}
+
+
diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/store.ts b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/store.ts
new file mode 100644
index 0000000000..a69272a70c
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/store.ts
@@ -0,0 +1,9 @@
+import { derived } from 'svelte/store';
+import { page } from '$app/stores';
+import type { Provider } from '../../store';
+
+export const provider = derived(
+ page,
+ // TODO: Set actual type
+ ($page) => $page.data.provider as Provider
+);
diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/updateName.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/updateName.svelte
new file mode 100644
index 0000000000..f77b626ca1
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/updateName.svelte
@@ -0,0 +1,51 @@
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/updateStatus.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/updateStatus.svelte
new file mode 100644
index 0000000000..f4fddcc744
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/updateStatus.svelte
@@ -0,0 +1,367 @@
+
+
+
+
+
+
+
+
+
Provider:
+
Channel:
+
Created: {toLocaleDateTime($provider.$createdAt)}
+
+
+
+
+
+
+ configure()}>Configure
+ updateStatus()}
+ >Update
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/providers/store.ts b/src/routes/console/project-[project]/messaging/providers/store.ts
new file mode 100644
index 0000000000..68e518b744
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/store.ts
@@ -0,0 +1,382 @@
+import { writable } from 'svelte/store';
+import type { Column } from '$lib/helpers/types';
+import { Providers } from '../provider.svelte';
+import { ProviderTypes } from '../providerType.svelte';
+
+export let showCreate = writable(false);
+
+export const columns = writable([
+ { id: '$id', title: 'Provider ID', type: 'string', show: true },
+ { id: 'name', title: 'Name', type: 'string', show: true },
+ { id: 'provider', title: 'Provider', type: 'string', show: true },
+ { id: 'type', title: 'Type', type: 'string', show: true },
+ { id: 'enabled', title: 'Status', type: 'boolean', show: true }
+]);
+
+type ProvidersMap = {
+ [key in ProviderTypes]: {
+ name: string;
+ text: string;
+ icon: string;
+ providers: {
+ [key in Providers]?: {
+ imageIcon: string;
+ title: string;
+ description: string;
+ configure: {
+ label: string;
+ name: string;
+ type: 'text' | 'phone' | 'email' | 'domain' | 'file' | 'switch';
+ placeholder?: string;
+ description?: string;
+ popover?: string[];
+ allowedFileExtensions?: string[];
+ }[];
+ };
+ };
+ };
+};
+
+export const providers: ProvidersMap = {
+ [ProviderTypes.Push]: {
+ name: 'Push notification',
+ text: 'notifications',
+ icon: 'device-mobile',
+ providers: {
+ [Providers.FCM]: {
+ imageIcon: 'firebase',
+ title: 'FCM',
+ description: 'Firebase Cloud Messaging',
+ configure: [
+ {
+ label: 'Server key (.json file)',
+ name: 'serverKey',
+ type: 'file',
+ allowedFileExtensions: ['json'],
+ placeholder: 'Enter server key',
+ popover: [
+ 'How to get the FCM server key? ',
+ 'Head to Project settings -> Service accounts -> Generate new private key. ',
+ 'Generating the new key will result in the download of a JSON file.'
+ ]
+ }
+ ]
+ },
+ [Providers.APNS]: {
+ imageIcon: 'apple',
+ title: 'APNS',
+ description: 'Apple Push Notification Service',
+ configure: [
+ {
+ label: 'Team ID',
+ name: 'teamId',
+ type: 'text',
+ placeholder: 'Enter team ID',
+ popover: [
+ 'How to get the team ID? ',
+ 'Head to Apple Developer Member Center -> Membership details -> Team ID. '
+ ]
+ },
+ {
+ label: 'Bundle ID',
+ name: 'bundleId',
+ type: 'text',
+ placeholder: 'Enter bundle ID',
+ popover: [
+ 'How to get the bundle ID? ',
+ 'Head to Apple Developer Member Center -> Certificates, Identifiers & Profiles -> Identifiers. ',
+ `
+
+
+
+
+ `
+ ]
+ },
+ {
+ label: 'Authentication key ID',
+ name: 'authKeyId',
+ type: 'text',
+ placeholder: 'Enter key ID',
+ popover: [
+ 'How to get the auth key ID? ',
+ 'Head to Apple Developer Member Center -> Certificates, Identifiers & Profiles -> Keys. ',
+ 'Click on your key to view details.'
+ ]
+ },
+ {
+ label: 'Auth key (.p8 file)',
+ name: 'authKey',
+ type: 'file',
+ allowedFileExtensions: ['p8'],
+ popover: [
+ 'How to get the authentication key? ',
+ 'Head to Apple Developer Member Center (under Program resources) -> Certificates, Identifiers & Profiles -> Keys. ',
+ 'Create a key and give it a name. Enable the Apple Push Notifications service (APNS), and register your key.'
+ ]
+ }
+ ]
+ }
+ // [Providers.MQTT]: {
+ // imageIcon: 'mqtt',
+ // title: 'MQTT',
+ // description: 'Message Queuing Telemtry Transport'
+ // }
+ }
+ },
+ [ProviderTypes.Email]: {
+ name: 'Email',
+ text: 'emails',
+ icon: 'mail',
+ providers: {
+ [Providers.Mailgun]: {
+ imageIcon: 'mailgun',
+ title: 'Mailgun',
+ description: '',
+ configure: [
+ {
+ label: 'API key',
+ name: 'apiKey',
+ type: 'text',
+ placeholder: 'Enter API key',
+ popover: [
+ 'How to get the API key? ',
+ 'Create an account in Mailgun.',
+ 'Head to Profile -> API Security -> Add new key. '
+ ]
+ },
+ {
+ label: 'Domain',
+ name: 'domain',
+ type: 'domain',
+ placeholder: 'Enter domain',
+ popover: [
+ 'How to create a domain? ',
+ 'Head to Sending -> Domains -> Add new domain. ',
+ 'Follow Mailgun instructions to verify the domain name.'
+ ]
+ },
+ {
+ label: 'EU region',
+ name: 'isEuRegion',
+ type: 'switch',
+ description:
+ 'Enable the EU region setting if your domain is within the European Union.'
+ },
+ {
+ label: 'Sender email',
+ name: 'from',
+ type: 'email',
+ placeholder: 'Enter email'
+ }
+ ]
+ },
+ [Providers.Sendgrid]: {
+ imageIcon: 'sendgrid',
+ title: 'Sendgrid',
+ description: '',
+ configure: [
+ {
+ label: 'API key',
+ name: 'apiKey',
+ type: 'text',
+ placeholder: 'Enter API key',
+ popover: [
+ 'How to get the API key? ',
+ 'Create an account in Mailgun.',
+ 'Head to Profile -> API Security -> Add new key. '
+ ]
+ },
+ {
+ label: 'Sender email',
+ name: 'from',
+ type: 'email',
+ placeholder: 'Enter email'
+ }
+ ]
+ }
+ }
+ },
+
+ [ProviderTypes.Sms]: {
+ name: 'SMS',
+ text: 'SMS',
+ icon: 'annotation',
+ providers: {
+ [Providers.Twilio]: {
+ imageIcon: 'twilio',
+ title: 'Twilio',
+ description: '',
+ configure: [
+ {
+ label: 'Account SID',
+ name: 'accountSid',
+ type: 'text',
+ placeholder: 'Enter Account SID',
+ popover: [
+ 'How to get the Account SID? ',
+ 'Head to Twilio console -> Account info -> Account SID. '
+ ]
+ },
+ {
+ label: 'Auth token',
+ name: 'authToken',
+ type: 'text',
+ placeholder: 'Enter Auth token',
+ popover: [
+ 'How to get the Auth token? ',
+ 'Head to Twilio console -> Account info -> Auth Token. '
+ ]
+ },
+ {
+ label: 'Sender number',
+ name: 'from',
+ type: 'phone',
+ placeholder: 'Enter phone',
+ popover: [
+ 'How to get sender number? ',
+ 'Head to Twilio console -> Account info -> My Twilio phone number. ',
+ 'If you have multiple Twilio phone numbers, you can select one as the default number.'
+ ]
+ }
+ ]
+ },
+ [Providers.Msg91]: {
+ imageIcon: 'msg91',
+ title: 'MSG91',
+ description: '',
+ configure: [
+ {
+ label: 'Auth key',
+ name: 'authKey',
+ type: 'text',
+ placeholder: 'Enter auth key',
+ popover: [
+ 'How to get the Auth key? ',
+ 'Create an account in MSG91.',
+ 'Click to open the Username dropdown -> Authkey -> Verify your mobile number -> Create Authkey. '
+ ]
+ },
+ {
+ label: 'Sender ID',
+ name: 'senderId',
+ type: 'text',
+ placeholder: 'Enter sender ID',
+ popover: [
+ 'How to create a Sender ID? ',
+ 'Head to MSG91 dashboard -> SMS -> Sender ID -> Create sender ID. '
+ ]
+ },
+ {
+ label: 'Sender number',
+ name: 'from',
+ type: 'phone',
+ placeholder: 'Enter phone'
+ }
+ ]
+ },
+ [Providers.Telesign]: {
+ imageIcon: 'telesign',
+ title: 'Telesign',
+ description: '',
+ configure: [
+ {
+ label: 'Username',
+ name: 'username',
+ type: 'text',
+ placeholder: 'Enter username'
+ },
+ {
+ label: 'Password',
+ name: 'password',
+ type: 'text',
+ placeholder: 'Enter password'
+ },
+ {
+ label: 'Sender number',
+ name: 'from',
+ type: 'phone',
+ placeholder: 'Enter phone'
+ }
+ ]
+ },
+ [Providers.Textmagic]: {
+ imageIcon: 'textmagic',
+ title: 'Textmagic',
+ description: '',
+ configure: [
+ {
+ label: 'API key',
+ name: 'apiKey',
+ type: 'text',
+ placeholder: 'Enter API key',
+ popover: [
+ 'How to get the API key? ',
+ 'Create an account in Textmagic.',
+ 'Head to TextMagic dashboard -> API Settings -> Add new API key. '
+ ]
+ },
+ {
+ label: 'Username',
+ name: 'username',
+ type: 'text',
+ placeholder: 'Enter username'
+ },
+ {
+ label: 'Sender number',
+ name: 'from',
+ type: 'phone',
+ placeholder: 'Enter phone'
+ }
+ ]
+ },
+ [Providers.Vonage]: {
+ imageIcon: 'vonage',
+ title: 'Vonage',
+ description: '',
+ configure: [
+ {
+ label: 'API key',
+ name: 'apiKey',
+ type: 'text',
+ placeholder: 'Enter API key',
+ popover: [
+ 'How to get the API key? ',
+ 'Create an account in Vonage.',
+ 'Head to Vonage dashboard and copy the API key. '
+ ]
+ },
+ {
+ label: 'API secret',
+ name: 'apiSecret',
+ type: 'text',
+ placeholder: 'Enter API secret',
+ popover: [
+ 'How to get the API secret? ',
+ 'Head to Vonage dashboard and copy the API secret. '
+ ]
+ },
+ {
+ label: 'Sender number',
+ name: 'from',
+ type: 'phone',
+ placeholder: 'Enter phone'
+ }
+ ]
+ }
+ }
+ }
+};
diff --git a/src/routes/console/project-[project]/messaging/providers/table.svelte b/src/routes/console/project-[project]/messaging/providers/table.svelte
new file mode 100644
index 0000000000..2921c40ecf
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/table.svelte
@@ -0,0 +1,170 @@
+
+
+
+
+ d.$id)} />
+ {#each $columns as column}
+ {#if column.show}
+ {column.title}
+ {/if}
+ {/each}
+
+
+ {#each data.providers.providers as provider (provider.$id)}
+
+
+ {#each $columns as column}
+ {#if column.show}
+ {#if column.id === '$id'}
+ {#key $columns}
+
+ {provider.$id}
+
+ {/key}
+ {:else if column.id === 'provider'}
+
+
+
+ {:else if column.id === 'type'}
+
+
+
+ {:else if column.id === 'enabled'}
+
+
+ {#if provider.enabled}
+
+ {/if}
+
+ {provider.enabled ? 'enabled' : 'disabled'}
+
+
+
+ {:else}
+
+ {provider[column.id]}
+
+ {/if}
+ {/if}
+ {/each}
+
+ {/each}
+
+
+
+ 0}>
+
+
+
{selectedIds.length}
+
+
+ {selectedIds.length > 1 ? 'providers' : 'provider'}
+
+ selected
+
+
+
+
+
(selectedIds = [])}>Cancel
+
(showDelete = true)}>
+ Delete
+
+
+
+
+
+
+
+ Are you sure you want to delete {selectedIds.length}
+ {selectedIds.length > 1 ? 'providers' : 'provider'}?
+
+
+ (showDelete = false)} disabled={deleting}>Cancel
+ Delete
+
+
diff --git a/src/routes/console/project-[project]/messaging/providers/update.svelte b/src/routes/console/project-[project]/messaging/providers/update.svelte
new file mode 100644
index 0000000000..4f3c14c44f
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/update.svelte
@@ -0,0 +1,268 @@
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/providers/wizard/configure.svelte b/src/routes/console/project-[project]/messaging/providers/wizard/configure.svelte
new file mode 100644
index 0000000000..404b4f69ad
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/wizard/configure.svelte
@@ -0,0 +1,147 @@
+
+
+
+ Configure
+
+ Set up the credentials below to enable {providers[$providerType].providers[$provider].title}
+ for sending
+ {providers[$providerType].text}.
+
+
+ {#each inputs as input}
+ {#if input.type === 'text'}
+
+
+ {@html input.popover?.join(' ')}
+
+
+ {:else if input.type === 'email'}
+
+
+
+ {@html input.popover?.join(' ')}
+
+
+
+ {:else if input.type === 'domain'}
+
+
+
+ {@html input.popover?.join(' ')}
+
+
+
+ {:else if input.type === 'phone'}
+
+
+
+ {@html input.popover?.join(' ')}
+
+
+
+ {:else if input.type === 'file'}
+
+
+
+ {@html input.popover?.join(' ')}
+
+
+
+ {:else if input.type === 'switch'}
+
+
+ {input.description}
+
+
+ {/if}
+ {/each}
+
+
+ Need a hand?
+
+
+
+
+
+
+ Read the full guide in the documentation
+
+
+
+
+
+
+
+
+
+ Invite a team member to complete this step
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/providers/wizard/provider.svelte b/src/routes/console/project-[project]/messaging/providers/wizard/provider.svelte
new file mode 100644
index 0000000000..1f9e238a1a
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/wizard/provider.svelte
@@ -0,0 +1,161 @@
+
+
+
+ Provider
+
+
+
+ {#if !showCustomId}
+
+
(showCustomId = !showCustomId)}
+ >
+ Provider ID
+
+
+ {:else}
+
+ {/if}
+
+ Select a provider you would like to enable for sending {providers[$providerType].text}.
+
+
+ {#each Object.entries(providers[$providerType].providers) as [value, option]}
+
+ {option.title}
+ {#if option.description}
+ {option.description}
+ {/if}
+
+ {/each}
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/providers/wizard/store.ts b/src/routes/console/project-[project]/messaging/providers/wizard/store.ts
new file mode 100644
index 0000000000..9f47f8a41a
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/wizard/store.ts
@@ -0,0 +1,103 @@
+import type { Providers } from '../../provider.svelte';
+import type { ProviderTypes } from '../../providerType.svelte';
+import { writable } from 'svelte/store';
+
+type ProviderParams = {
+ providerId: string;
+ name: string;
+ default: boolean;
+ enabled: boolean;
+};
+
+/**
+ * SMS providers
+ */
+
+export type TwilioProviderParams = ProviderParams & {
+ accountSid: string;
+ authToken: string;
+ from: string;
+};
+
+export type Msg91ProviderParams = ProviderParams & {
+ from: string;
+ senderId: string;
+ authKey: string;
+};
+
+export type TelesignProviderParams = ProviderParams & {
+ from: string;
+ username: string;
+ password: string;
+};
+
+export type TextmagicProviderParams = ProviderParams & {
+ from: string;
+ username: string;
+ apiKey: string;
+};
+
+export type VonageProviderParams = ProviderParams & {
+ from: string;
+ apiKey: string;
+ apiSecret: string;
+};
+
+/**
+ * Email providers
+ */
+
+export type MailgunProviderParams = ProviderParams & {
+ isEuRegion: boolean;
+ from: string;
+ apiKey: string;
+ domain: string;
+};
+
+export type SendgridProviderParams = ProviderParams & {
+ from: string;
+ apiKey: string;
+};
+
+/**
+ * Push providers
+ */
+
+export type FCMProviderParams = ProviderParams & {
+ serverKey: string;
+};
+
+export type APNSProviderParams = ProviderParams & {
+ authKey: string;
+ authKeyId: string;
+ teamId: string;
+ bundleId: string;
+};
+
+export type MQTTProviderParams = ProviderParams & {
+ serverKey: string;
+};
+
+export const providerType = writable(null);
+export const provider = writable(null);
+export const providerParams = writable<{
+ twilio: Partial;
+ msg91: Partial;
+ telesign: Partial;
+ textmagic: Partial;
+ vonage: Partial;
+ mailgun: Partial;
+ sendgrid: Partial;
+ fcm: Partial;
+ apns: Partial;
+}>({
+ twilio: null,
+ msg91: null,
+ telesign: null,
+ textmagic: null,
+ vonage: null,
+ mailgun: null,
+ sendgrid: null,
+ fcm: null,
+ apns: null
+});
diff --git a/src/routes/console/project-[project]/messaging/providers/wizard/store.ts.bak b/src/routes/console/project-[project]/messaging/providers/wizard/store.ts.bak
new file mode 100644
index 0000000000..0ee3ba46ca
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/providers/wizard/store.ts.bak
@@ -0,0 +1,161 @@
+import { writable } from 'svelte/store';
+import type { Column } from '$lib/components/viewSelector.svelte';
+
+export let showCreate = writable(false);
+
+export const columns = writable([
+ { id: '$id', title: 'Provider ID', show: true },
+ { id: 'name', title: 'Name', show: true },
+ { id: 'provider', title: 'Provider', show: true },
+ { id: 'channel', title: 'Channel', show: true },
+ { id: 'status', title: 'Status', show: true }
+]);
+
+export type Instruction = {
+ text: string;
+ input: {
+ label: string;
+ name: string;
+ type: 'text' | 'domain' | 'email';
+ placeholder: string;
+ };
+};
+
+export const providers = {
+ sms: {
+ name: 'SMS',
+ text: 'SMS',
+ icon: 'annotation',
+ providers: {
+ twilio: {
+ imageIcon: 'twilio',
+ title: 'Twilio',
+ description: ''
+ },
+ msg91: {
+ imageIcon: 'msg91',
+ title: 'MSG91',
+ description: ''
+ },
+ telesign: {
+ imageIcon: 'telesign',
+ title: 'Telesign',
+ description: ''
+ },
+ textmagic: {
+ imageIcon: 'textmagic',
+ title: 'Textmagic',
+ description: ''
+ },
+ vonage: {
+ imageIcon: 'vonage',
+ title: 'Vonage',
+ description: ''
+ }
+ }
+ },
+ email: {
+ name: 'Email',
+ text: 'emails',
+ icon: 'mail',
+ providers: {
+ mailgun: {
+ imageIcon: 'mailgun',
+ title: 'Mailgun',
+ description: '',
+ initialize: [
+ {
+ text: 'Before you can create a Mailgun provider, you need to first create a Mailgun account.'
+ },
+ {
+ text: 'Head to your Profile > API Security .'
+ },
+ {
+ text: 'Generate a key and give it a name. Copy and paste it in the field below.',
+ input: {
+ label: 'API key',
+ name: 'apiKey',
+ type: 'text',
+ placeholder: 'Enter API key'
+ }
+ },
+ {
+ // TODO: Update link to domain verification
+ text: 'Head to Sending > Domains and click on \'Add New Domain\'. Verify your domain by following the instructions .',
+ input: {
+ label: 'Base URL',
+ name: 'baseUrl',
+ type: 'text',
+ placeholder: 'Enter base URL'
+ }
+ }
+ ],
+ configure: [
+ {
+ text: 'Provide a display name your recipient will see when they receive your emails.',
+ input: {
+ label: 'From',
+ name: 'from',
+ type: 'text',
+ placeholder: 'Enter name'
+ }
+ },
+ {
+ text: 'Provide an email address that will be visible to the recipient as the senders email address for this message.',
+ input: {
+ label: 'From email address',
+ name: 'email',
+ type: 'email',
+ placeholder: 'Enter email'
+ }
+ },
+ {
+ text: 'Provide an email address for users to use when replying to your emails.',
+ input: {
+ label: 'Reply to',
+ name: 'replyTo',
+ type: 'email',
+ placeholder: 'Enter email'
+ }
+ },
+ {
+ text: 'Provide the domain as it is registered on Mailgun.',
+ input: {
+ label: 'Domain',
+ name: 'domain',
+ type: 'domain',
+ placeholder: 'Enter domain'
+ }
+ }
+ ]
+ },
+ sendgrid: {
+ imageIcon: 'sendgrid',
+ title: 'Sendgrid',
+ description: ''
+ }
+ }
+ },
+ push: {
+ name: 'Push notification',
+ text: 'notifications',
+ icon: 'device-mobile',
+ providers: {
+ fcm: {
+ imageIcon: 'firebase',
+ title: 'FCM',
+ description: 'Firebase Cloud Messaging'
+ },
+ apns: {
+ imageIcon: 'apple',
+ title: 'APNS',
+ description: 'Apple Push Notification Service'
+ },
+ mqtt: {
+ imageIcon: 'mqtt',
+ title: 'MQTT',
+ description: 'Message Queuing Telemtry Transport'
+ }
+ }
+ }
+};
diff --git a/src/routes/console/project-[project]/messaging/push-notification-preview-dark.svg b/src/routes/console/project-[project]/messaging/push-notification-preview-dark.svg
new file mode 100644
index 0000000000..777dec2679
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/push-notification-preview-dark.svg
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/push-notification-preview-light.svg b/src/routes/console/project-[project]/messaging/push-notification-preview-light.svg
new file mode 100644
index 0000000000..7fd66545dd
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/push-notification-preview-light.svg
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/pushPhone.svelte b/src/routes/console/project-[project]/messaging/pushPhone.svelte
new file mode 100644
index 0000000000..a6fb893f77
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/pushPhone.svelte
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+ {$project.name}
+
+
now
+
+
+
{title || 'Message Title'}
+
+ {body || 'Enter your message in the input field on the left to see it here'}
+
+
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/sms-preview-dark.svg b/src/routes/console/project-[project]/messaging/sms-preview-dark.svg
new file mode 100644
index 0000000000..cafc7a33bc
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/sms-preview-dark.svg
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/sms-preview-light.svg b/src/routes/console/project-[project]/messaging/sms-preview-light.svg
new file mode 100644
index 0000000000..9e9099038a
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/sms-preview-light.svg
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/smsPhone.svelte b/src/routes/console/project-[project]/messaging/smsPhone.svelte
new file mode 100644
index 0000000000..dfa2f82615
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/smsPhone.svelte
@@ -0,0 +1,124 @@
+
+
+
+
+ {getIntials($project.name)}
+
+
{$project.name}
+
+
+ Today {getTime()}
+
+
+
+ {content || 'Enter your message in the input field on the left to see it here'}
+
+
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/store.ts b/src/routes/console/project-[project]/messaging/store.ts
new file mode 100644
index 0000000000..d30c67b808
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/store.ts
@@ -0,0 +1,265 @@
+import type { Column } from '$lib/helpers/types';
+import { Providers } from './provider.svelte';
+import { ProviderTypes } from './providerType.svelte';
+import { writable } from 'svelte/store';
+
+export const showCreate = writable(false);
+
+export const columns = writable([
+ { id: '$id', title: 'Message ID', type: 'string', show: true, width: 140 },
+ { id: 'description', title: 'Description', type: 'string', show: true, width: 140 },
+ { id: 'message', title: 'Message', type: 'string', show: false, width: 140 },
+ { id: 'providerType', title: 'Type', type: 'string', show: true, width: 100 },
+ { id: 'status', title: 'Status', type: 'string', show: true, width: 120 },
+ { id: 'scheduledAt', title: 'Scheduled at', type: 'datetime', show: true, width: 120 },
+ { id: 'deliveredAt', title: 'Delivered at', type: 'datetime', show: false, width: 120 }
+]);
+
+export const targetsById = writable>({});
+export const topicsById = writable>({});
+
+// TODO: remove this when the SDK and API are ready
+export type Message = {
+ $id: string;
+ $createdAt: string;
+ $updatedAt: string;
+ topics: string[];
+ users: string[];
+ targets: string[];
+ scheduledAt: string;
+ deliveredAt: string;
+ deliveryErrors: string[];
+ deliveredTo: number;
+ status: string;
+ providerType: ProviderTypes;
+ description: string;
+ data: {
+ content?: string;
+ body?: string;
+ subject?: string;
+ title?: string;
+ };
+};
+
+// TODO: remove this when the SDK and API are ready
+export const messages: { messages: Message[]; total: number } = {
+ messages: [
+ {
+ $id: '637a40ba7a703e3936e1',
+ $createdAt: '2021-08-31T12:00:00.000Z',
+ $updatedAt: '2021-08-31T12:00:00.000Z',
+ description: 'Welcome',
+ providerType: ProviderTypes.Push,
+ topics: [],
+ users: ['user-1', 'user-2'],
+ targets: [],
+ scheduledAt: '2021-03-01T00:00:00.000Z',
+ deliveredAt: '2021-03-01T00:00:00.000Z',
+ deliveryErrors: [],
+ deliveredTo: 2,
+ status: 'sent',
+ data: {
+ title: 'Welcome to the Cloud',
+ body: 'Detailed body'
+ }
+ },
+ {
+ $id: '637a40ba7a703e3936e2',
+ $createdAt: '2021-08-31T12:00:00.000Z',
+ $updatedAt: '2021-08-31T12:00:00.000Z',
+ description: 'Public beta announcement',
+ providerType: ProviderTypes.Sms,
+ topics: [],
+ users: ['user-1', 'user-2'],
+ targets: [],
+ scheduledAt: '2021-03-01T00:00:00.000Z',
+ deliveredAt: '2021-03-01T00:00:00.000Z',
+ deliveryErrors: [],
+ deliveredTo: 2,
+ status: 'scheduled',
+ data: {
+ content: 'Cloud is live on public beta'
+ }
+ },
+ {
+ $id: '637a40ba7a703e3936e3',
+ $createdAt: '2021-08-31T12:00:00.000Z',
+ $updatedAt: '2021-08-31T12:00:00.000Z',
+ description: 'Welcome',
+ providerType: ProviderTypes.Email,
+ topics: [],
+ users: ['user-1', 'user-2'],
+ targets: [],
+ scheduledAt: '',
+ deliveredAt: '',
+ deliveryErrors: [],
+ deliveredTo: 2,
+ status: 'draft',
+ data: {
+ subject: 'Welcome to the Cloud',
+ content: 'Detailed content'
+ }
+ }
+ ],
+ total: 3
+};
+
+// TODO: remove this when the SDK and API are ready
+export type Provider = {
+ $id: string;
+ $createdAt: string;
+ $updatedAt: string;
+ name: string;
+ provider: Providers;
+ default: boolean;
+ enabled: boolean;
+ type: ProviderTypes;
+ credentials: object;
+ options: object;
+};
+
+// TODO: remove this when the SDK and API are ready
+export const providersById: { [providerId: string]: Provider } = {
+ '637a40ba7a703e3936e1': {
+ $id: '637a40ba7a703e3936e1',
+ $createdAt: '2021-08-31T12:00:00.000Z',
+ $updatedAt: '2021-08-31T12:00:00.000Z',
+ name: 'My Firebase',
+ provider: Providers.FCM,
+ type: ProviderTypes.Push,
+ default: false,
+ enabled: true,
+ credentials: {},
+ options: {}
+ },
+ '637a40ba7a703e3936e2': {
+ $id: '637a40ba7a703e3936e2',
+ $createdAt: '2021-08-31T12:00:00.000Z',
+ $updatedAt: '2021-08-31T12:00:00.000Z',
+ name: 'My APNS',
+ provider: Providers.APNS,
+ type: ProviderTypes.Push,
+ default: false,
+ enabled: false,
+ credentials: {},
+ options: {}
+ },
+ '637a40ba7a703e3936e3': {
+ $id: '637a40ba7a703e3936e3',
+ $createdAt: '2021-08-31T12:00:00.000Z',
+ $updatedAt: '2021-08-31T12:00:00.000Z',
+ name: 'My MQTT',
+ provider: Providers.MQTT,
+ type: ProviderTypes.Push,
+ default: false,
+ enabled: false,
+ credentials: {},
+ options: {}
+ },
+ '637a40ba7a703e3936e4': {
+ $id: '637a40ba7a703e3936e4',
+ $createdAt: '2021-08-31T12:00:00.000Z',
+ $updatedAt: '2021-08-31T12:00:00.000Z',
+ name: 'My Sendgrid',
+ provider: Providers.Sendgrid,
+ type: ProviderTypes.Email,
+ default: false,
+ enabled: false,
+ credentials: {},
+ options: {}
+ },
+ '637a40ba7a703e3936e5': {
+ $id: '637a40ba7a703e3936e5',
+ $createdAt: '2021-08-31T12:00:00.000Z',
+ $updatedAt: '2021-08-31T12:00:00.000Z',
+ name: 'My Mailgun',
+ provider: Providers.Mailgun,
+ type: ProviderTypes.Email,
+ default: false,
+ enabled: false,
+ credentials: {},
+ options: {}
+ },
+ '637a40ba7a703e3936e6': {
+ $id: '637a40ba7a703e3936e6',
+ $createdAt: '2021-08-31T12:00:00.000Z',
+ $updatedAt: '2021-08-31T12:00:00.000Z',
+ name: 'My Telesign',
+ provider: Providers.Telesign,
+ type: ProviderTypes.Sms,
+ default: false,
+ enabled: false,
+ credentials: {},
+ options: {}
+ },
+ '637a40ba7a703e3936e7': {
+ $id: '637a40ba7a703e3936e7',
+ $createdAt: '2021-08-31T12:00:00.000Z',
+ $updatedAt: '2021-08-31T12:00:00.000Z',
+ name: 'My Msg91',
+ provider: Providers.Msg91,
+ type: ProviderTypes.Sms,
+ default: false,
+ enabled: false,
+ credentials: {},
+ options: {}
+ },
+ '637a40ba7a703e3936e8': {
+ $id: '637a40ba7a703e3936e8',
+ $createdAt: '2021-08-31T12:00:00.000Z',
+ $updatedAt: '2021-08-31T12:00:00.000Z',
+ name: 'My Textmagic',
+ provider: Providers.Textmagic,
+ type: ProviderTypes.Sms,
+ default: false,
+ enabled: false,
+ credentials: {},
+ options: {}
+ },
+ '637a40ba7a703e3936e9': {
+ $id: '637a40ba7a703e3936e9',
+ $createdAt: '2021-08-31T12:00:00.000Z',
+ $updatedAt: '2021-08-31T12:00:00.000Z',
+ name: 'My Vonage',
+ provider: Providers.Vonage,
+ type: ProviderTypes.Sms,
+ default: false,
+ enabled: false,
+ credentials: {},
+ options: {}
+ },
+ '637a40ba7a703e3936f0': {
+ $id: '637a40ba7a703e3936f0',
+ $createdAt: '2021-08-31T12:00:00.000Z',
+ $updatedAt: '2021-08-31T12:00:00.000Z',
+ name: 'My Twilio',
+ provider: Providers.Twilio,
+ type: ProviderTypes.Sms,
+ default: false,
+ enabled: false,
+ credentials: {},
+ options: {}
+ }
+};
+
+// TODO: remove when sdk has the model
+export type Topic = {
+ $id: string;
+ $createdAt: string;
+ $updatedAt: string;
+ providerId: string;
+ name: string;
+ total: number;
+ description: string;
+};
+
+export type Target = {
+ $id: string;
+ $createdAt: string;
+ $updatedAt: string;
+ name: string;
+ userId: string;
+ providerId: string;
+ providerType: ProviderTypes;
+ identifier: string;
+};
diff --git a/src/routes/console/project-[project]/messaging/topics/+page.svelte b/src/routes/console/project-[project]/messaging/topics/+page.svelte
new file mode 100644
index 0000000000..2b86aee240
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/+page.svelte
@@ -0,0 +1,106 @@
+
+
+
+
+
+
Topics
+
+ ($showCreate = true)} event="create_topic">
+
+ Create topic
+
+
+
+
+
+
+
+
+ ($showCreate = true)} event="create_topic">
+
+ Create topic
+
+
+
+
+
+ {#if data.topics.total}
+
+
+
+ {:else if $hasPageQueries}
+
+
+ {:else if data.search && data.search != 'empty'}
+
+
+
Sorry, we couldn't find '{data.search}'
+
There are no topics that match your search.
+
+
+ Clear Search
+
+
+ {:else}
+
+ ($showCreate = true)}
+ href="https://appwrite.io/docs/references/cloud/client-web/teams"
+ target="topic" />
+ {/if}
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/+page.ts b/src/routes/console/project-[project]/messaging/topics/+page.ts
new file mode 100644
index 0000000000..013a089ad5
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/+page.ts
@@ -0,0 +1,53 @@
+import { Query } from '@appwrite.io/console';
+import { sdk } from '$lib/stores/sdk';
+import { getLimit, getPage, getQuery, getSearch, pageToOffset } from '$lib/helpers/load';
+import { Dependencies, PAGE_LIMIT } from '$lib/constants';
+import { queryParamToMap, queries } from '$lib/components/filters';
+import type { Topic } from '../store';
+
+export const load = async ({ depends, url, route }) => {
+ depends(Dependencies.MESSAGING_TOPICS);
+ const page = getPage(url);
+ const search = getSearch(url);
+ const limit = getLimit(url, route, PAGE_LIMIT);
+ const offset = pageToOffset(page, limit);
+ const query = getQuery(url);
+
+ const parsedQueries = queryParamToMap(query || '[]');
+ queries.set(parsedQueries);
+
+ const payload = {
+ queries: [
+ Query.limit(limit),
+ Query.offset(offset),
+ Query.orderDesc(''),
+ ...parsedQueries.values()
+ ]
+ };
+
+ if (search) {
+ payload['search'] = search;
+ }
+
+ // TODO: remove when the API is ready with data
+ // This allows us to mock w/ data and when search returns 0 results
+ const topics: { topics: Topic[]; total: number } = await sdk.forProject.client.call(
+ 'GET',
+ new URL(sdk.forProject.client.config.endpoint + '/messaging/topics'),
+ {
+ 'X-Appwrite-Project': sdk.forProject.client.config.project,
+ 'content-type': 'application/json',
+ 'X-Appwrite-Mode': 'admin'
+ },
+ payload
+ );
+
+ return {
+ offset,
+ limit,
+ search,
+ query,
+ page,
+ topics
+ };
+};
diff --git a/src/routes/console/project-[project]/messaging/topics/create.svelte b/src/routes/console/project-[project]/messaging/topics/create.svelte
new file mode 100644
index 0000000000..2bb52245e2
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/create.svelte
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+ {#if !showCustomId}
+
+
(showCustomId = !showCustomId)}
+ >
+ Topic ID
+
+
+ {:else}
+
+ {/if}
+
+
+ (showCreate = false)}>Cancel
+ Create
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/store.ts b/src/routes/console/project-[project]/messaging/topics/store.ts
new file mode 100644
index 0000000000..0c950c385b
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/store.ts
@@ -0,0 +1,12 @@
+import type { Column } from '$lib/helpers/types';
+import { writable } from 'svelte/store';
+
+export let showCreate = writable(false);
+
+export const columns = writable([
+ { id: '$id', title: 'Topic ID', type: 'string', show: true, width: 140 },
+ { id: 'name', title: 'Name', type: 'string', show: true, width: 140 },
+ { id: 'description', title: 'Description', type: 'string', show: true, width: 140 },
+ { id: 'total', title: 'Subscribers', type: 'integer', show: true, width: 140 },
+ { id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 140 }
+]);
diff --git a/src/routes/console/project-[project]/messaging/topics/table.svelte b/src/routes/console/project-[project]/messaging/topics/table.svelte
new file mode 100644
index 0000000000..c72533d667
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/table.svelte
@@ -0,0 +1,154 @@
+
+
+
+
+ d.$id)} />
+ {#each $columns as column}
+ {#if column.show}
+ {column.title}
+ {/if}
+ {/each}
+
+
+ {#each data.topics.topics as topic (topic.$id)}
+
+
+
+ {#each $columns as column (column.id)}
+ {#if column.show}
+ {#if column.id === '$id'}
+ {#key $columns}
+
+ {topic.$id}
+
+ {/key}
+ {:else if column.type === 'datetime'}
+
+ {#if !topic[column.id]}
+ -
+ {:else}
+ {toLocaleDateTime(topic[column.id])}
+ {/if}
+
+ {:else}
+
+ {topic[column.id]}
+
+ {/if}
+ {/if}
+ {/each}
+
+ {/each}
+
+
+
+ 0}>
+
+
+
{selectedIds.length}
+
+
+ {selectedIds.length > 1 ? 'topics' : 'topic'}
+
+ selected
+
+
+
+
+
(selectedIds = [])}>Cancel
+
(showDelete = true)}>
+ Delete
+
+
+
+
+
+
+
+ Are you sure you want to delete {selectedIds.length}
+ {selectedIds.length > 1 ? 'topics' : 'topic'}?
+
+
+ (showDelete = false)} disabled={deleting}>Cancel
+ Delete
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+layout.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+layout.svelte
new file mode 100644
index 0000000000..525eedb90f
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+layout.svelte
@@ -0,0 +1,5 @@
+
+ Topic - Appwrite
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+layout.ts b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+layout.ts
new file mode 100644
index 0000000000..594acc96ba
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+layout.ts
@@ -0,0 +1,30 @@
+import type { LayoutLoad } from './$types';
+import Breadcrumbs from './breadcrumbs.svelte';
+import Header from './header.svelte';
+import { sdk } from '$lib/stores/sdk';
+import { Dependencies } from '$lib/constants';
+import { error } from '@sveltejs/kit';
+
+export const load: LayoutLoad = async ({ params, depends }) => {
+ depends(Dependencies.MESSAGING_TOPIC);
+
+ const response = await sdk.forProject.client.call(
+ 'GET',
+ new URL(sdk.forProject.client.config.endpoint + '/messaging/topics/' + params.topic),
+ {
+ 'X-Appwrite-Project': sdk.forProject.client.config.project,
+ 'content-type': 'application/json',
+ 'X-Appwrite-Mode': 'admin'
+ }
+ );
+
+ try {
+ return {
+ header: Header,
+ breadcrumbs: Breadcrumbs,
+ topic: response
+ };
+ } catch (e) {
+ throw error(e.code, e.message);
+ }
+};
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+page.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+page.svelte
new file mode 100644
index 0000000000..dd88db1906
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+page.svelte
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/activity/+page.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/activity/+page.svelte
new file mode 100644
index 0000000000..f805a79f04
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/activity/+page.svelte
@@ -0,0 +1,8 @@
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/activity/+page.ts b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/activity/+page.ts
new file mode 100644
index 0000000000..97f3d29aeb
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/activity/+page.ts
@@ -0,0 +1,34 @@
+import { Query, type Models } from '@appwrite.io/console';
+import { sdk } from '$lib/stores/sdk';
+import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
+import { PAGE_LIMIT } from '$lib/constants';
+import type { PageLoad } from './$types';
+
+export const load: PageLoad = async ({ params, url, route }) => {
+ const page = getPage(url);
+ const limit = getLimit(url, route, PAGE_LIMIT);
+ const offset = pageToOffset(page, limit);
+
+ const payload = {
+ queries: [Query.limit(limit), Query.offset(offset)]
+ };
+
+ // TODO: remove when the API is ready with data
+ // This allows us to mock w/ data and when search returns 0 results
+ const logs: Models.LogList = await sdk.forProject.client.call(
+ 'GET',
+ new URL(`${sdk.forProject.client.config.endpoint}/messaging/topics/${params.topic}/logs`),
+ {
+ 'X-Appwrite-Project': sdk.forProject.client.config.project,
+ 'content-type': 'application/json',
+ 'X-Appwrite-Mode': 'admin'
+ },
+ payload
+ );
+
+ return {
+ offset,
+ limit,
+ logs
+ };
+};
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/breadcrumbs.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/breadcrumbs.svelte
new file mode 100644
index 0000000000..c41df553b9
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/breadcrumbs.svelte
@@ -0,0 +1,27 @@
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/dangerZone.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/dangerZone.svelte
new file mode 100644
index 0000000000..113c2550cd
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/dangerZone.svelte
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+ Delete topic
+
+
+ The topic will be permanently deleted, including all data associated with this topic. This
+ action is irreversible.
+
+
+
+
+ {$topic.name}
+
+
+ {$topic.total} subscriber{$topic.total === 1 ? '' : 's'}
+
+
+
+
+
+ ($showDelete = true)} event="delete_messaging_provider"
+ >Delete
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/deleteTopic.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/deleteTopic.svelte
new file mode 100644
index 0000000000..a3e4226e33
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/deleteTopic.svelte
@@ -0,0 +1,56 @@
+
+
+
+
+ Are you sure you want to delete {$topic.name} from '{$project.name}'?
+
+
+ (showDelete = false)}>Cancel
+ Delete
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/details.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/details.svelte
new file mode 100644
index 0000000000..1e81e5044e
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/details.svelte
@@ -0,0 +1,19 @@
+
+
+
+
+ Details
+
+
+
+
+
{$topic.total} subscriber{$topic.total === 1 ? '' : 's'}
+
Created: {toLocaleDateTime($topic.$createdAt)}
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/header.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/header.svelte
new file mode 100644
index 0000000000..0848e48db2
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/header.svelte
@@ -0,0 +1,48 @@
+
+
+
+
+
+ {$topic.name}
+
+ {$topic.$id}
+
+
+
+ {#each tabs as tab}
+
+ {tab.title}
+
+ {/each}
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/store.ts b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/store.ts
new file mode 100644
index 0000000000..a2379dadd2
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/store.ts
@@ -0,0 +1,9 @@
+import { derived } from 'svelte/store';
+import { page } from '$app/stores';
+import type { Topic } from '../../store';
+
+export const topic = derived(
+ page,
+ // TODO: Set actual type
+ ($page) => $page.data.topic as Topic
+);
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/+page.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/+page.svelte
new file mode 100644
index 0000000000..4a05081d43
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/+page.svelte
@@ -0,0 +1,166 @@
+
+
+
+
+
+
Subscribers
+
+ (showAdd = true)} event="create_subscriber">
+
+ Add subscriber
+
+
+
+
+
+
+
+
+ (showAdd = true)} event="create_subscriber">
+
+ Add subscriber
+
+
+
+
+
+ {#if data.subscribers.total}
+
+
+
+ {:else if $hasPageQueries}
+
+
+ {:else if data.search && data.search != 'empty'}
+
+
+
Sorry, we couldn't find '{data.search}'
+
There are no subscribers that match your search.
+
+
+ Clear Search
+
+
+ {:else}
+
+ (showAdd = true)}
+ href="https://appwrite.io/docs/references/cloud/client-web/teams"
+ target="subscriber" />
+ {/if}
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/+page.ts b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/+page.ts
new file mode 100644
index 0000000000..2011433bf3
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/+page.ts
@@ -0,0 +1,69 @@
+import { Query } from '@appwrite.io/console';
+import { sdk } from '$lib/stores/sdk';
+import { getLimit, getPage, getQuery, getSearch, pageToOffset } from '$lib/helpers/load';
+import { Dependencies, PAGE_LIMIT } from '$lib/constants';
+import type { PageLoad } from './$types';
+import type { Target } from '../../../store';
+import { queryParamToMap, queries } from '$lib/components/filters';
+
+export type Subscriber = {
+ $id: string;
+ $createdAt: string;
+ $updatedAt: string;
+ targetId: string;
+ target: Target;
+ userName: string;
+ topicId: string;
+};
+
+export const load: PageLoad = async ({ params, url, route, depends, parent }) => {
+ depends(Dependencies.MESSAGING_TOPIC_SUBSCRIBERS);
+ const page = getPage(url);
+ const limit = getLimit(url, route, PAGE_LIMIT);
+ const offset = pageToOffset(page, limit);
+ const search = getSearch(url);
+ const query = getQuery(url);
+
+ const parsedQueries = queryParamToMap(query || '[]');
+ queries.set(parsedQueries);
+
+ const payload = {
+ queries: [
+ Query.limit(limit),
+ Query.offset(offset),
+ Query.orderDesc(''),
+ ...parsedQueries.values()
+ ]
+ };
+
+ if (search) {
+ payload['search'] = search;
+ }
+
+ const { topic } = await parent();
+
+ // TODO: remove when the API is ready with data
+ // This allows us to mock w/ data and when search returns 0 results
+ const subscribers: { subscribers: Subscriber[]; total: number } =
+ await sdk.forProject.client.call(
+ 'GET',
+ new URL(
+ `${sdk.forProject.client.config.endpoint}/messaging/topics/${params.topic}/subscribers`
+ ),
+ {
+ 'X-Appwrite-Project': sdk.forProject.client.config.project,
+ 'content-type': 'application/json',
+ 'X-Appwrite-Mode': 'admin'
+ },
+ payload
+ );
+
+ return {
+ offset,
+ limit,
+ search,
+ query,
+ topic,
+ subscribers
+ };
+};
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/store.ts b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/store.ts
new file mode 100644
index 0000000000..ca6538023a
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/store.ts
@@ -0,0 +1,11 @@
+import type { Column } from '$lib/helpers/types';
+import { writable } from 'svelte/store';
+
+export const columns = writable([
+ { id: '$id', title: 'Subscriber ID', type: 'string', show: true, width: 140 },
+ { id: 'userName', title: 'Name', type: 'string', show: true, width: 100 },
+ { id: 'targetId', title: 'Target ID', type: 'string', show: true, width: 140 },
+ { id: 'target', title: 'Target', type: 'string', show: true, filter: false, width: 140 },
+ { id: 'type', title: 'Type', type: 'string', show: true, filter: false, width: 80 },
+ { id: '$createdAt', title: 'Created', type: 'string', show: true, width: 100 }
+]);
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/table.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/table.svelte
new file mode 100644
index 0000000000..13fbbf7f0a
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/table.svelte
@@ -0,0 +1,191 @@
+
+
+
+
+ d.$id)} />
+ {#each $columns as column}
+ {#if column.show}
+ {column.title}
+ {/if}
+ {/each}
+
+
+ {#each data.subscribers.subscribers as subscriber (subscriber.$id)}
+ {@const target = subscriber.target}
+
+
+
+ {#each $columns as column}
+ {#if column.show}
+ {#if column.id === '$id'}
+ {#key $columns}
+
+
+ {subscriber.$id}
+
+
+ {/key}
+ {:else if column.id === 'targetId'}
+
+
+ {subscriber[column.id]}
+
+
+ {:else if column.id === 'target'}
+
+ {#if target.providerType === ProviderTypes.Push}
+ {target.name}
+ {:else}
+ {target.identifier}
+ {/if}
+
+ {:else if column.id === 'type'}
+
+
+
+ {:else if column.id === '$createdAt'}
+
+ {toLocaleDateTime(subscriber[column.id])}
+
+ {:else}
+
+ {subscriber[column.id]}
+
+ {/if}
+ {/if}
+ {/each}
+
+ {/each}
+
+
+
+ 0}>
+
+
+
{selectedIds.length}
+
+
+ {selectedIds.length > 1 ? 'subscribers' : 'subscriber'}
+
+ selected
+
+
+
+
+
(selectedIds = [])}>Cancel
+
(showDelete = true)}>
+ Delete
+
+
+
+
+
+
+
+ Are you sure you want to delete {selectedIds.length}
+ {selectedIds.length > 1 ? 'subscribers' : 'subscriber'}?
+
+
+ (showDelete = false)} disabled={deleting}>Cancel
+ Delete
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/updateDescription.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/updateDescription.svelte
new file mode 100644
index 0000000000..137e7944c1
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/updateDescription.svelte
@@ -0,0 +1,65 @@
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/updateName.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/updateName.svelte
new file mode 100644
index 0000000000..dfc6dd1af4
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/updateName.svelte
@@ -0,0 +1,66 @@
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/topicsModal.svelte b/src/routes/console/project-[project]/messaging/topicsModal.svelte
new file mode 100644
index 0000000000..f1141b26e0
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/topicsModal.svelte
@@ -0,0 +1,167 @@
+
+
+
+ Select existing topics you want to send this message to its recipients.
+
+ {#if Object.keys(topicResultsById).length > 0}
+ {#each Object.entries(topicResultsById) as [topicId, topic]}
+ onTopicSelection(event, topic)}>
+
+
+
+ {topic.name}
+ ({topic.total} subscribers)
+
+
+
+ {/each}
+
+
Total results: {totalResults}
+
+
+ {:else if search}
+
+
+
+
Sorry we couldn't find "{search}"
+
There are no topics that match your search.
+
+
+ Documentation
+ (search = '')}>Clear search
+
+
+
+ {:else}
+
+
+
+
+ You have no topics. Create a topic to see them here.
+
+
+
+ Need a hand? Learn more in our
+ documentation .
+
+
+
+
+ {/if}
+
+
+ Add
+
+
diff --git a/src/routes/console/project-[project]/messaging/userTargetsModal.svelte b/src/routes/console/project-[project]/messaging/userTargetsModal.svelte
new file mode 100644
index 0000000000..878e77a277
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/userTargetsModal.svelte
@@ -0,0 +1,252 @@
+
+
+
+ Grant access to any authenticated or anonymous user.
+
+ {#if Object.keys(userResultsById).length > 0}
+
+ {#each Object.entries(userResultsById) as [userId, user]}
+ {@const selectedCount = user.targets.filter(
+ (target) => selected[target.$id]
+ ).length}
+
+
+ 0 &&
+ user.targets.every((target) => targetsById[target.$id])}
+ checked={selectedCount > 0 && selectedCount == user.targets.length}
+ on:change={(event) => onUserSelection(event, userId)} />
+
+
+
+
+ {#if user.name}
+ {user.name}
+ {:else if user.email}
+ {user.email}
+ {:else if user.phone}
+ {user.phone}
+ {:else}
+ {userId}
+ {/if}
+
+
+
+
+ ({selectedCount} targets)
+
+
+ {#each user.targets as target}
+
+
onTargetSelection(event, target)}>
+
+
+
+ {#if target.providerType !== ProviderTypes.Push}
+ {target.identifier}
+ {:else}
+
+ {target.name}
+ {/if}
+
+
+
+
+ {/each}
+
+
+ {/each}
+
+
+
Total results: {totalResults}
+
+
+ {:else if search}
+
+
+
+
Sorry we couldn't find "{search}"
+
There are no Users that match your search.
+
+
+ Documentation
+ (search = '')}>Clear search
+
+
+
+ {:else}
+
+
+
+
+ You have no users. Create a user to see them here.
+
+
+ Need a hand? Learn more in our
+ documentation .
+
+
+
+
+ {/if}
+
+
+ Add
+
+
diff --git a/src/routes/console/project-[project]/messaging/wizard/emailFormList.svelte b/src/routes/console/project-[project]/messaging/wizard/emailFormList.svelte
new file mode 100644
index 0000000000..950c749f8e
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/wizard/emailFormList.svelte
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Enter the email address to which te test message will be
+ (selected = 'other')}
+ on:keyup|self={clickOnEnter}
+ role="button"
+ tabindex="0">
+
+
+
+
+
+
+ (showTest = false)}>Cancel
+ Send
+
+
+
+
+
+ Enable the HTML mode if your message contains HTML tags.
+
+
+
+
+ {#if !showCustomId}
+
+
(showCustomId = !showCustomId)}
+ >
+ Message ID
+
+
+ {:else}
+
+ {/if}
+
diff --git a/src/routes/console/project-[project]/messaging/wizard/pushFormList.svelte b/src/routes/console/project-[project]/messaging/wizard/pushFormList.svelte
new file mode 100644
index 0000000000..9329272b46
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/wizard/pushFormList.svelte
@@ -0,0 +1,250 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Enter the phone number to which the test message will be
+ (selected = 'other')}
+ on:keyup|self={clickOnEnter}
+ role="button"
+ tabindex="0">
+
+
+
+
+
+
+ (showTest = false)}>Cancel
+ Send
+
+
+
+
+
+
+ {#if !showCustomId}
+
+
(showCustomId = !showCustomId)}
+ >
+ Message ID
+
+
+ {:else}
+
+ {/if}
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/wizard/smsFormList.svelte b/src/routes/console/project-[project]/messaging/wizard/smsFormList.svelte
new file mode 100644
index 0000000000..7f784b4d22
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/wizard/smsFormList.svelte
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Enter the phone number to which the test message will be
+ (selected = 'other')}
+ on:keyup|self={clickOnEnter}
+ role="button"
+ tabindex="0">
+
+
+
+
+
+
+ (showTest = false)}>Cancel
+ Send
+
+
+
+
+
+ {#if !showCustomId}
+
+
(showCustomId = !showCustomId)}
+ >
+ Message ID
+
+
+ {:else}
+
+ {/if}
+
+
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/wizard/step1.svelte b/src/routes/console/project-[project]/messaging/wizard/step1.svelte
new file mode 100644
index 0000000000..4fffcc771d
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/wizard/step1.svelte
@@ -0,0 +1,30 @@
+
+
+
+ Message
+
+
+ Create an {providers[$providerType].text} that will be displayed to your subscribers. Learn more
+ in our documentation.
+
+ {#if $providerType === ProviderTypes.Email}
+
+ {:else if $providerType === ProviderTypes.Sms}
+
+ {:else if $providerType === ProviderTypes.Push}
+
+ {/if}
+
diff --git a/src/routes/console/project-[project]/messaging/wizard/step2.svelte b/src/routes/console/project-[project]/messaging/wizard/step2.svelte
new file mode 100644
index 0000000000..cb456bc95e
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/wizard/step2.svelte
@@ -0,0 +1,149 @@
+
+
+
+ Targets
+
+ Select users to whom this message should be directed.
+ {#if targetIdsLength === 0 && topicIdsLength === 0}
+
+
+
+ (showDropdown = !showDropdown)}>
+
+
+
+
+ Select recipients to get started
+
+
+
+ {:else}
+
+
+
+ Target
+
+
+
+ {#each Object.entries($topicsById) as [topicId, topic] (topicId)}
+
+
+
+
+
+ {topic.name}
+ ({topic.total} subscribers)
+
+
+
+
+
+ removeTopic(topicId)}>
+
+
+
+
+
+ {/each}
+ {#each Object.entries($targetsById) as [targetId, target] (targetId)}
+
+
+
+ {target.name ? target.name : target.identifier}
+
+
+
+
+
+ removeTarget(targetId)}>
+
+
+
+
+
+ {/each}
+
+
+
+
+ (showDropdown = !showDropdown)}>
+
+ Add
+
+
+ {/if}
+
diff --git a/src/routes/console/project-[project]/messaging/wizard/step3.svelte b/src/routes/console/project-[project]/messaging/wizard/step3.svelte
new file mode 100644
index 0000000000..4a62c75ef2
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/wizard/step3.svelte
@@ -0,0 +1,101 @@
+
+
+
+ Schedule
+
+ Schedule the time you want your users to receive this message. Learn more in our
+ documentation.
+
+
+
+
+
+
+
+
+ {#if when === 'now'}
+ The message will be sent immediately
+ {:else if !dateTime || isNaN(dateTime.getTime())}
+ The message will be sent later
+ {:else}
+ The message will be sent at {dateTime.toLocaleString('en', formatOptions)}
+ {/if}
+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/wizard/store.ts b/src/routes/console/project-[project]/messaging/wizard/store.ts
new file mode 100644
index 0000000000..3b02644524
--- /dev/null
+++ b/src/routes/console/project-[project]/messaging/wizard/store.ts
@@ -0,0 +1,52 @@
+import { ProviderTypes } from '../providerType.svelte';
+import { writable } from 'svelte/store';
+import type { Target } from '../store';
+
+export enum MessageStatuses {
+ DRAFT = 'draft',
+ PROCESSING = 'processing'
+}
+
+export type MessageParams = {
+ messageId: string;
+ topics: string[];
+ users: string[];
+ targets: string[];
+ description: string;
+ status: MessageStatuses;
+ scheduledAt?: string;
+};
+
+export type EmailMessageParams = MessageParams & {
+ subject: string;
+ content: string;
+ html: boolean;
+};
+
+export type SMSMessageParams = MessageParams & {
+ content: string;
+};
+
+export type PushMessageParams = MessageParams & {
+ title: string;
+ body: string;
+ data: [string, string][];
+ action?: string;
+ icon?: string;
+ sound?: string;
+ color?: string;
+ tag?: string;
+ badge?: string;
+};
+
+export const providerType = writable(null);
+export const targetsById = writable>({});
+export const messageParams = writable<{
+ [ProviderTypes.Email]: Partial;
+ [ProviderTypes.Sms]: Partial;
+ [ProviderTypes.Push]: Partial;
+}>({
+ [ProviderTypes.Email]: null,
+ [ProviderTypes.Sms]: null,
+ [ProviderTypes.Push]: null
+});
diff --git a/static/images/apns-bundle-id.png b/static/images/apns-bundle-id.png
new file mode 100644
index 0000000000..90fde1eff2
Binary files /dev/null and b/static/images/apns-bundle-id.png differ