Introducing: The New Mail Pilot
by Alexander Obenauer
in
Announcements
published
March 24, 2020
by Alexander Obenauer
in
Announcements
published
March 24, 2020

For the last three years, I’ve been working on a totally new Mail Pilot. More than that, it’s a totally new way to do email.

Today, I want to share the first look at what I’ve been cooking up:

This new Mail Pilot represents a dramatic shift in every part of building and using an email client, but I’ll save that and more for the coming weeks.

If you want to stay up to date with the new Mail Pilot, sign up on the website:

👉 Head to mailpilot.app to sign up for more info as it’s available

Right now, the new Mail Pilot is being tested by 1,000 customers who pre-ordered the app. It’s been in testing with increasing batches of pre-order customers since last year, and I’ve been working closely with them to make sure we’re developing the right solution for today’s problems.

Many of the conversations we’ve had in our Slack workspace have led to some pretty fundamental additions or changes. Having a community of early users in Slack has been a huge part of building this new generation of Mail Pilot apps.

The community has always been responsible for making Mail Pilot happen, from its earliest days on Kickstarter, to its more recent reboot. Building with the Mail Pilot community is a natural fit, and it serves to make this Mail Pilot truly great.

I can’t wait to share more with you, so stay tuned. More to come in the weeks ahead!

Build and deploy a production-ready to-do app in under an hour with Userbase and Svelte
by Alexander Obenauer
in
Learn to Code
published
February 6, 2020
by Alexander Obenauer
in
Learn to Code
published
February 6, 2020

In this start-to-finish tutorial, we'll build a production-grade to-do app with Userbase, a new BaaS offering, and Svelte, a fantastic way to build web apps.

This article assumes you have some familiarity with web development, but little to no familiarity with Userbase or Svelte.

We'll start with some background on these technologies, and then dive into the process.

What is Userbase

Userbase just launched as a wild new backend-as-a-service offering which keeps user data encrypted end-to-end, and sports a dramatically simple API.

It allows you to build and ship simple HTML, CSS, and Javascript - an entirely static website - with production-grade user authentication and data storage, without having to worry about any of the backend.

What is Svelte

Svelte has been around for some time, and remains my favorite way to build front-end apps.

Imagine if you had a language where you could declare not just what a variable's value should be, but instead how it should be computed. What if that declaration then kept the value up-to-date at all times, and updated anything that depended on it at the same time. 

var values = []
store.subscribe((newValues) => values = newValues)
dynamic var count = values.length
...
<div id="count">{count}</div>

Imagine if our dynamic var is always updated automatically when values is updated. And imagine if our div's contents were then, also, automatically updated.

Wild, right?

Well, that's what Svelte is. Technically its own language that operates within the confines of Javascript syntax, along with the compiler that converts that language into plain Javascript which automatically keeps everything in your app up-to-date as state changes occur.

It's the best of both worlds: a declarative syntax like we enjoy in React, but with the efficiency of no framework (not only do Svelte apps run faster, users don't have to unwittingly download massive amounts of Javascript. Imagine using a UI library in Svelte; it's a dev dependency! Only what you actually use ever gets included in what is sent to users' browsers).

Let's start building

Seriously, this should take less than an hour. Here we go.

Step 1: Create a Userbase account

Head to Userbase.com, click "Try it free", and set up a free account.

It'll start you off with a trial app. Keep this window open, we'll use the "App ID" soon.

Step 2: Create a Svelte project

Install npm on your machine if you haven't before.

In your terminal, run this command to prepare and run a starter Svelte app:

npx degit sveltejs/template TodoApp
cd TodoApp
npm install
npm run dev

That last line, npm run dev is what you call from the TodoApp directory whenever you want to run your app for development purposes.

Open http://localhost:5000 to see the starter Svelte app running.

Step 3: Build Userbase access into a Svelte store

Svelte has a fantastic stores feature that we'll use here to interact with our user's accounts and data in Userbase.

If you've got Atom installed, you can run atom ./ in a new terminal tab to open the code editor loaded with the TodoApp project files.

3.1 Include the Userbase JS SDK

Open TodoApp/public/index.html, and include Userbase's JS SDK above the bundle.js line:

<script type="text/javascript" src="https://sdk.userbase.com/1/userbase.js"></script>

The file should now look like this:

TodoApp/public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset='utf-8'>
    <meta name='viewport' content='width=device-width,initial-scale=1'>

    <title>Svelte app</title>

    <link rel='icon' type='image/png' href='/favicon.png'>
    <link rel='stylesheet' href='/global.css'>
    <link rel='stylesheet' href='/build/bundle.css'>

    <script type="text/javascript" src="https://sdk.userbase.com/1/userbase.js"></script>
    <script defer src='/build/bundle.js'></script>
</head>

<body>
</body>
</html>

3.2 Create the Svelte stores for accessing Userbase

Inside TodoApp/src, a directory already made for you, create a new directory called Stores. We're going to create two Svelte stores inside that directory.

Create the UserAccount store

Inside TodoApp/src/Stores, create a new file called UserAccount.js. Paste the following into it:

TodoApp/src/Stores/UserAccount.js

import { writable } from 'svelte/store';

/** STATE **/
/// This simply defines the initial state, and serves as a guide for the structure

const state = {
  initialized: false,
  signedIn: false,
  username: undefined,
  error: undefined
}

/** ACTION CREATORS **/
/// These are the only things exposed to the outside world

const actionCreators = (store) => {
  return {
    initialize: () => {
      userbase.init({ appId: 'YOUR_APP_ID_HERE' })
            .then((session) => {
                if (session.user) {
                    store.dispatch(SignedIn(session.user.username))
                } else {
            store.dispatch(SignedOut())
                }
            })
            .catch((e) => store.dispatch(Error(e)))
            .finally(() => store.dispatch(Initialized()))
    },
    signUp: (username, password, rememberMe) => {
      userbase.signUp({ username, password, rememberMe: rememberMe ? 'local' : '' }) /* TODO: Check into rememberMe functionality */
        .then((user) => store.dispatch(SignedIn(username)))
        .catch((e) => store.dispatch(Error(e)))
    },
    signIn: (username, password, rememberMe) => {
      userbase.signIn({ username, password, rememberMe: rememberMe ? 'local' : '' }) /* TODO: Check into rememberMe functionality */
        .then((user) => store.dispatch(SignedIn(username)))
        .catch((e) => store.dispatch(Error(e)))
    },
    signOut: () => {
        userbase.signOut()
            .then(() => store.dispatch(SignedOut()))
            .catch((e) => store.dispatch(Error(e)))
    }
  }
}

/** ACTIONS **/
/// These actions are those passed to the reducer by action creators. They are not exported out of this file.

const Initialized = () => ({ name: "Initialized" })
const SignedIn = (username) => ({ name: "SignedIn", username })
const SignedOut = () => ({ name: "SignedOut" })
const Error = (error) => ({ name: "Error", error })

/** REDUCER **/
/// Receives internal actions as sent by action creators

const reducer = (state, action) => {
  switch (action.name) {
    case "Initialized":
      return { ...state,
        initialized: true
      };

    case "SignedIn":
      return { ...state,
        username: action.username,
        signedIn: true,
        error: undefined
      };

    case "SignedOut":
      return { ...state,
        username: undefined,
        signedIn: false,
        error: undefined
      };

    case "Error":
      return { ...state,
        error: action.error
      }

    default:
      return state;
  }
};

/** FLUX STORE **/
/// A simple flux store that allows dispatching to update the state in a svelte store with a reducer

class Store {
  constructor(subscribe, updateState, reducer) {
    this.updateState = updateState
    this.reducer = reducer

    this.unsubscribe = subscribe(newState => this.state = newState)
  }

  dispatch(action) {
    this.updateState(s => this.reducer(s, action))
  }
}

/** SVELTE STORE **/

function createUserAccountStore() {
    const { subscribe, update, set } = writable(state);
  const store = new Store(subscribe, update, reducer)
  const actions = actionCreators(store)

    return {
    ...actions,
        subscribe
    };
}

export const userAccountStore = createUserAccountStore();

Make sure to replace YOUR_APP_ID_HERE with the app ID from Userbase

This code produces a Svelte store which allows the code that uses it to take advantage of all the first-class features of Svelte stores (we'll see all that in just a minute).

Internally, it updates its state with an architecture resembiling a Flux store for better maintainability.


3.3 Create the Todos store

Inside TodoApp/src/Stores, create a new file called Todos.js. Paste the following into it:

TodoApp/src/Stores/Todos.js

import { writable } from 'svelte/store';

/** STATE **/
/// This simply defines the initial state, and serves as a guide for the structure

const state = {
  initialized: false,
  todos: []
}

const todo = {
  title: '',
  complete: false
}

/** ACTION CREATORS **/
/// These are the only things exposed to the outside world

const actionCreators = (store) => {
  return {
    initialize: () => {
      userbase.openDatabase({
        databaseName: 'todos',
        changeHandler: (todos) => store.dispatch(TodosLoaded(todos))
      })
      .catch((e) => store.dispatch(Error(e)))
      .finally(() => store.dispatch(Initialized()))
    },
    createTodo: (item) => {
      item.title = item.title || ""

      userbase.insertItem({
        databaseName: 'todos',
        item
      })
      .catch((e) => store.dispatch(Error(e)))
    },

    updateTodo: (todo, item) => {
      userbase.updateItem({ databaseName: 'todos', itemId: todo.itemId, item: {
        ...todo.item,
        ...item
      }})
      .catch((e) => store.dispatch(Error(e)))
    },

    deleteTodo: (itemId) => {
      userbase.deleteItem({ databaseName: 'todos', itemId: itemId })
        .catch((e) => store.dispatch(Error(e)))
    }
  }
}

/** ACTIONS **/
/// These actions are those passed to the reducer by action creators. They are not exported out of this file.

const Initialized = () => ({ name: "Initialized" })
const TodosLoaded = (todos) => ({ name: "TodosLoaded", todos })
const Error = (error) => ({ name: "Error", error })

/** REDUCER **/
/// Receives internal actions as sent by action creators

const reducer = (state, action) => {
  switch (action.name) {
    case "Initialized":
      return { ...state,
        initialized: true
      };

    case "TodosLoaded":
      return { ...state,
        todos: action.todos
      };

    case "Error":
      return { ...state,
        error: action.error
      }

    default:
      return state;
  }
};

/** FLUX STORE **/
/// A simple flux store that allows dispatching to update the state in a svelte store with a reducer

class Store {
  constructor(subscribe, updateState, reducer) {
    this.updateState = updateState
    this.reducer = reducer

    this.unsubscribe = subscribe(newState => this.state = newState)
  }

  dispatch(action) {
    this.updateState(s => this.reducer(s, action))
  }
}

/** SVELTE STORE **/

function createTodosStore() {
    const { subscribe, update, set } = writable(state);
  const store = new Store(subscribe, update, reducer)
  const actions = actionCreators(store)

    return {
    ...actions,
        subscribe
    };
}

export const todosStore = createTodosStore();

This also produces a Svelte store which allows the code that uses it to take advantage of all the first-class features of Svelte stores.

Just like the UserAccount store, it also updates its internal state with an architecture resembling a Flux store for better maintainability.


Step 4: Build the app interface

Since Userbase is taking care of all the backend needs of our app, we primarily only need to build the interface.

4.1 The fundamentals

Open TodoApp/src/App.svelte. This is where we are going to begin building our app's interface.

You can see this file already has some code in it. Let's go ahead and replace that with this:

TodoApp/src/App.svelte

<script>
    // Library imports
    import { onMount } from 'svelte';

    // Component imports
    import SignUp from './SignUp.svelte';
    import SignIn from './SignIn.svelte';
    import TodoList from './TodoList.svelte';

    // State
    import { userAccountStore } from './Stores/UserAccount.js';
    $: initialized = $userAccountStore.initialized
    $: signedIn = $userAccountStore.signedIn
    $: username = $userAccountStore.username
    $: error = $userAccountStore.error


    // Events

    onMount(() => {
        userAccountStore.initialize()
    })

    function signOut() {
        userAccountStore.signOut()
    }

</script>

<main>
    <h1 id="logo">TODO</h1>

    <div id="content">
        {#if initialized}
            {#if signedIn}
                <TodoList />
            {:else}
                <SignIn />
                <div class="divider"></div>
                <SignUp />
            {/if}
        {:else}
            Loading...
        {/if}

        {#if error}<div id="error">{error}</div>{/if}
    </div>

    {#if signedIn}
        <div id="logout">
            <button on:click={signOut}>Logout {username}</button>
        </div>
    {/if}
</main>

<style>
    main {
        text-align: center;
        padding: 50px 0;
    }

    #content {
        padding: 20px;
        max-width: 360px;
        margin: 0 auto;

        background: #FFFFFF;
        box-shadow: 0 2px 4px 0 rgba(0,0,0,0.05), 0 12px 50px 0 rgba(0,0,0,0.15);
        border-radius: 12px;
    }

    #logout {
        margin-top: 20px;
    }

    h1 {
        color: #ff3e00;
        text-transform: uppercase;
        font-size: 4em;
        font-weight: 100;
        margin: 20px 0;
    }

    .divider {
        border-top: 1px dotted rgba(0,0,0,0.25);
        margin: 50px 0px;
    }
</style>

This is all pretty straightforward, but a few notes:

onMount is one of Svelte's lifecycle methods (more here). There are a number of them you can use to run code at specific points in the component's lifecycle.

$: is how you declare a variable which should automatically stay up to date (instead of var, let, or const). Note: there is even more than just declarations that $: can do, more on that here.

$anySvelteStore automatically subscribes to that store (and unsubscribes from it when the component is destroyed), providing its value wherever used.

So putting those two together:

$: username = $userAccountStore.username

This line will always ensure that username is up-to-date with the latest value from our UserAccount store to give us a currently signed-in user's username.

Finally, in Svelte, "HTML" can also contain other Svelte components, as well as variables from your <script> section.

So we can use that always-updated username value in our Svelte HTML:

<button on:click={signOut}>Logout {username}</button>

The button will always, automatically reflect the user's current username, even if it changes. Automatically!

Now we did use a number of other Svelte components which don't actually exist yet, so you'll see in your first terminal tab that the compiler is showing an error. Let's fix that by building out the rest of the app's interface.


4.2 The other components

Now let's build out our other components. We're going to create five custom Svelte components. For each, create the new file with the right file name and paste the contents into it.

TodoApp/src/SignIn.svelte

<script>
  import { userAccountStore } from './Stores/UserAccount.js';

  var username;
  var password;

  function handleSignIn(e) {
    e.preventDefault()

    userAccountStore.signIn(username, password, true) // TODO: In future versions, only pass rememberMe as true if the user checks a box
  }
</script>

<!-- HTML -->
<h1>Sign In</h1>
<form id="signin-form" on:submit={handleSignIn}>
  <input id="signin-username" type="text" required placeholder="Username" bind:value={username}>
  <input id="signin-password" type="password" required placeholder="Password" bind:value={password}>
  <input type="submit" value="Sign in">
</form>
{#if $userAccountStore.error}
  <div id="error">{$userAccountStore.error}</div>
{/if}

<style>
  #signin-form input {
    display: block;
    margin: 10px auto;
  }
</style>

TodoApp/src/SignUp.svelte

<script>
  import { userAccountStore } from './Stores/UserAccount.js';

  var username;
  var password;

  function handleSignUp(e) {
    e.preventDefault()

    userAccountStore.signUp(username, password, true) // TODO: In future versions, only pass rememberMe as true if the user checks a box
  }
</script>

<!-- HTML -->
<h1>Create an account</h1>
<form id="signup-form" on:submit={handleSignUp}>
  <input id="signup-username" type="text" required placeholder="Username" bind:value={username}>
  <input id="signup-password" type="password" required placeholder="Password" bind:value={password}>
  <input type="submit" value="Create an account">
</form>
{#if $userAccountStore.error}
  <div id="error">{$userAccountStore.error}</div>
{/if}

<style>
  #signup-form input {
    display: block;
    margin: 10px auto;
  }
</style>

TodoApp/src/TodoList.svelte

<script>
  // Library imports
  import { onMount } from 'svelte';

  // Component imports
  import TodoRow from './TodoRow.svelte';

  // State
  import { todosStore } from './Stores/Todos.js';
  $: initialized = $todosStore.todos
  $: error = $todosStore.error
  $: todos = $todosStore.todos || []

  let newTodoTitleValue;

  // Events

  onMount(() => {
    todosStore.initialize()
  })

  function addNewTodo(e) {
    e.preventDefault();

    todosStore.createTodo({
      title: newTodoTitleValue
    })

    newTodoTitleValue = ""
  }

</script>


<!-- HTML -->

<div id="todoList">
  {#each todos as todo, index (todo.itemId)}
    <TodoRow todo={todo} />
  {/each}

  <form id="addTodo" on:submit={addNewTodo}>
    <input id="addTodoInput" type="text" required placeholder="Add a new todo" bind:value={newTodoTitleValue}>
    <input type="submit" value="Add">
  </form>
</div>

{#if !initialized}
  <div id="loading">Loading to-dos...</div>
{/if}

{#if error}
  <div id="error">{error}</div>
{/if}

<style>
  #todoList {
    text-align: left;
    font-size: 14px;
  }

  form#addTodo {
    margin-top: 20px;
    display: flex;
  }

  form#addTodo #addTodoInput {
    flex-grow: 1;
    margin-right: 5px;
  }
</style>

TodoApp/src/TodoRow.svelte

<script>
  // Props
  export let todo;

  // Library imports
  import { afterUpdate } from 'svelte';

  // Component imports
  import Checkbox from './Checkbox.svelte';

  // Dynamic state
  import { todosStore } from './Stores/Todos.js';

  $: title = todo.item.title
  $: complete = todo.item.complete

  // Events

  function toggledCheckbox(e, todo) {
    e.preventDefault()
    todosStore.updateTodo(todo, { complete: !todo.item.complete })
  }
</script>

<!-- HTML -->
<div class={complete ? "todo complete" : "todo"} on:click>
  <Checkbox value={complete ? "checked" : "unchecked"} on:click={(e) => {toggledCheckbox(e, todo)}} />
  <p>{title}</p>
</div>

<style>
  .complete {
    opacity: 0.5;
  }

  .todo p {
    padding: 6px 0;
    padding-left: 22px;
    margin: 0;

    font-size: 16px;
    line-height: 20px;
  }
</style>

TodoApp/src/Checkbox.svelte

<script>
  // Props
  export let value;
</script>

<!-- HTML -->
{#if value == "none"}
  <div class="noCheckbox"></div>
{:else}
  <div class="container" on:click>
    {#if value == "checked"}<input type="checkbox" checked="checked">{:else}<input type="checkbox">{/if}
    <span class="checkmark"></span>
  </div>
{/if}

<style>
.noCheckbox {
  min-width: 22px;
  flex-grow: 0;
}

.container {
  min-width: 22px;
  flex-grow: 0;

  display: block;
  position: relative;
}

.container input {
  position: absolute;
  opacity: 0;
  cursor: pointer;
  height: 0;
  width: 0;
}

.checkmark {
  border-radius: 3px;
  position: absolute;
  top: 8px;
  left: 0;
  height: 14px;
  width: 14px;
  border: 1px solid rgba(142,161,174,0.5);
  transition: all 0.2s ease;
}

.container:hover input ~ .checkmark {
  border: 1px solid rgba(142,161,174,1);
}

.container input:checked ~ .checkmark {
  border: 1px solid transparent;
  background-color: #2196F3;
}

.checkmark:after {
  content: "";
  position: absolute;
  display: none;
}

.container input:checked ~ .checkmark:after {
  display: block;
}

.container .checkmark:after {
  left: 5px;
  top: 3px;
  width: 2px;
  height: 5px;
  border: solid white;
  border-width: 0 2px 2px 0;
  -webkit-transform: rotate(45deg);
  -ms-transform: rotate(45deg);
  transform: rotate(45deg);
}
</style>

A few notes on all of this code:

SignIn and SignUp repeat the same CSS. This is because Svelte keeps CSS from affecting any components outside of the one it is declared within. This helps you avoid all kinds of odd issues that can come up from the cascading part of CSS.

To avoid repeating CSS that you actually want to be global, put your global styles in /public/global.css.

But better yet, since the SignIn and SignUp components are so similar, you may want to simply combine them into one component, depending on how you would want your app's user authentication interface to work.

You can see our use of props which we pass to our custom components TodoRow and Checkbox. This is how state moves down into children components.

You can see our use of bindings in TodoList.svelte:

<input id="addTodoInput" type="text" required placeholder="Add a new todo" bind:value={newTodoTitleValue}>

By putting in bind:value={newTodoTitleValue}, we are creating a two-way binding with the variable newTodoTitleValue. This is how we get state into a parent component from a child component, and how we might update that child component with a new value. You can see both of these things happening in the addNewTodo function, where we first use the value of newTodoTitleValue, which is what the user has typed in, and then we set the value of newTodoTitleValue to an empty string, which clears the input field.

Finally, Checkbox dispatches its own event by forwarding the on:click event to one of its divs. For how to make custom component events, see more here.

Other than that, everything else largely follows the conventions we've already discussed.


4.3 Run your app

With that, your app should compile and run! If the compiler doesn't automatically attempt to recompile, hit CTRL+C in terminal, and npm run dev again.

Once it has compiled, you can load up your app at http://localhost:5000/, create an account, log out, sign back in, create tasks, and mark them as complete. If you refresh your browser window, you'll see that everything is persisted in your account, and if you open a second browser window and add or complete tasks, you'll see the first browser window automatically stay up to date.

This magic is all available thanks to Userbase's brilliant design and simple SDK, hooked into a lot of Svelte's magic which makes this starter app a fantastic foundation which can scale into a significantly more complex app without significantly more complex code.


Next steps

For a deeper understanding of Svelte, run through their fantastic tutorial at https://svelte.dev/tutorial/basics.

For a deeper understanding of Userbase, run through their Quickstart or read their docs at https://userbase.com/docs/.

With just these two resources, you can scale up to a very sophisticated production-ready app in very little time.

Happy creating!

In this start-to-finish tutorial, we'll build a production-grade to-do app with Userbase, a new BaaS offering, and Svelte, a fantastic way to build web apps.

This article assumes you have some familiarity with web development, but little to no familiarity with Userbase or Svelte.

We'll start with some background on these technologies, and then dive into the process.

Read the article →
Announcing the new Mail Pilot
by Alexander Obenauer
in
Announcements
published
January 19, 2020
by Alexander Obenauer
in
Announcements
published
January 19, 2020

In 2011, we first unveiled our vision for the future of email.

In 2017, we got to work on our second such vision.

To kick off 2020, we're making it official.

Mail Pilot Discovery Edition → The New Mail Pilot

It became clear during the last few years of development that what we were building in Mail Pilot Discovery Edition was much more than just the R&D flavor of our product. It was quickly becoming our new vision for the future of email. One that executes gracefully on today's problems & wants in email. So it will now drop the "Discovery Edition" moniker, and simply become the new Mail Pilot.

Mail Pilot 3 → Mail Pilot Classic

Mail Pilot 3, which many of you have on your Macs today, is the best-ever execution on the original ideas that made Mail Pilot popular. It will continue to be available, it will continue to be a one-time paid flavor of the app, it will continue to be developed, and it will sport a new name: Mail Pilot Classic. That's because it has all of the classic Mail Pilot features like reminders, unified lists, complete & incomplete, and set aside.

Simplify, simplify, simplify.

When our last website launched, we got a ton of questions. There were simply too many terms for too many things.

So we're going extremely simple: our home page now shows off the future: the new Mail Pilot. That's it. And as always, you can dive in for more.

Get the release

Right now, we're rolling out the app in stages.

Be sure to sign up for an invite at mailpilot.app. If you are part of the Yacht Club, you're already on the list!

We've got a lot to show you, and lots to come, so stay tuned.

Youll never look at email the same way again.
Eight years ago, we first reimagined email.
Now, we've done it again.
Thank you, we will be in touch soon!
Oops! Something went wrong while submitting the form.
“An ingenious new email service”
— David Pogue in the New York Times
“A good app, and well worth it.”
— Leo Laporte, This Week in Tech
“A joy to use”
— Bakari Chavanu, MakeUseOf
“Best designed email app on the App Store”
— Cam Bunton, Today’s iPhone
“Mail Pilot is a superb mail client”
— Dave Johnson, CBS MoneyWatch
“A clean, impressive, often beautiful way to manage unruly email”
— Nathan Alderman, Macworld