Three big announcements
by Alexander Obenauer
in
Announcements
published
September 2, 2020
by Alexander Obenauer
in
Announcements
published
September 2, 2020

We have three major things to share with you.

But first, a quick update: over the last six weeks, we've been sending out invites to our waitlist — there are still more to go out, so if you haven't gotten yours yet, stay tuned!

Alright, let's dive in:

Previewing the next frontier

Mail Pilot has always been meant for more than just the desktop. This summer, we previewed the future of Mail Pilot. Check out the video here →

Mail Pilot Swag

For the first time since our Kickstarter back in 2012, we are making Mail Pilot swag available. Head to the Mindsense Swag Shop for more →

Our next app

We're always working on something new at Mindsense, and for only the third time in our history, that is about to mean an entirely new product. Sign up for updates on our new product, Symphonies →

Introducing: The Batch List
by Alexander Obenauer
in
Announcements
published
September 2, 2020
by Alexander Obenauer
in
Announcements
published
September 2, 2020

The new Mail Pilot brings unprecedented calm and focus to the inbox, in large part because of the Batch List.

Check it out in the new video:

An update on development (May 2020)
by Alexander Obenauer
in
Announcements
published
June 14, 2020
by Alexander Obenauer
in
Announcements
published
June 14, 2020

Hey crew!

I'm working on the finishing touches of a rather large update now.

(Update: It's here!)

Some background:

Originally, I planned to put search in the "More" tab, and I expected it to work like MP3's search (which runs a local search for recent messages, and defaults to IMAP search for anything further).

During initial testing last year as we were discerning exactly how this new kind of email client should work, one of the major things that kept coming up was the need for wicked fast search. It was clear that it needed to be fast to activate, fast to use, fast to run the searches, and fast to get you into the results you're looking for.

Burying search in the more tab and relying on the notoriously slow IMAP search implementation on most email servers wasn't going to cut it.

So, much of the internals of our favorite email app needed an overhaul: in order to provide a wicked fast search experience, Mail Pilot needed to do a lot of new things.

Things like caching message headers locally in a database (effectively now becoming home to a traditional disconnected client's IMAP sync engine), to gracefully handling Gmail's chaotic implementation of labels-as-folders via IMAP (and the baffling decision to make the IMAP folder "[Gmail]/All Mail" be both the archive folder and also home to a copy of every message, a decision which requires code specific to Gmail accounts in email clients that want to handle search most effectively).

There were a lot of shortcuts available to make the re-engineerings, re-factorings, and new development go faster but result in a worse user experience. True to the purpose of this new generation of Mail Pilot, I decided against each and every shortcut available.

The result: a slow and steady development of the foundation that will power a world-class search experience, as well as a handful of other major features in future versions.

Nearing the end of the development, the foundation is almost set. I will kick off testing soon with a small group of folks. The update will roll out once we have pinned down all of the details of the search experience (and of course, ironed out all the bugs hiding trying to hide from me).

Stay tuned, and I hope you all are well!

This update was initially shared in the Mail Pilot Yacht Club Slack group.

How we organize, process, and prioritize user requests for our product
by Alexander Obenauer
in
Craft & Process
published
April 21, 2020
by Alexander Obenauer
in
Craft & Process
published
April 21, 2020

Managing many user requests at once with Mail Pilot is an entire system in itself. This essay breaks down the organization and process I use to hande and make use of a high volume of requests.

Email is a very personal thing. Everyone has their own unique list of “must-have” features for an email client. Managing many user requests at once is a skill that I have become proficient at, but only after years of gracelessly fumbling through poor processes.

Here are the organization, process, and other details that have served me well for the last several years as I’ve handled support alone.

Organization

Let’s start with the organization that I use, so that I can share my process using this framework as the foundation.

In Things 3, I have an ongoing project in which I have multiple lists (separated with headings):

Inbox. This is where I put new requests as they arrive, which I organize later.

Tier 1: Showstoppers. This is where I categorize anything that will be a major disruption to many users. Crashes, confusing bugs, etc.

Tier 2: Solutions for current existential crisis. I reserve Tier 2 for the existential crisis which I presently fear will cause everyone to stop using my app. Earlier in development, it was “Tier 2: A thousand cuts” when the app had many remaining little things which were far from showstoppers, but experienced all together orchestrated a symphony of frustration for the user. Once those “thousand cuts” were largely resolved, I moved into “Tier 2: Feels incomplete without” as the app is still young, and lacks some features that would make it feel complete. Tier 2 will always be the thing that I fear is the current biggest cause for app abandonment.

Tier 3: Really good to have. These are the things which I believe to be important to the direction of the product and needed soon. Like the above two tiers, these items may or may not have originated directly from user requests. Instead, Tier 3 is often home to important things which people would not know to request.

Tier 4: Requested by multiple people. These are the requests I have received from more than one person.

Tier 5: Requested once. These are requests I have only received once. This tends to be a long list, but once something is requested by a second person, it moves up to tier 4. This helps me keep track of what is being requested more regularly, and move those things up.

Deferred requests. These are the requests which I know I will not or cannot build at the present time for technical, resource, or other reasons.

Wild Ideas. These are the things which might not be actionable right now, but represent really interesting ideas that could be big features or changes for future releases.

Process

When I receive a new request, here is the process I go through. I do this for every kind of request — whether it’s a bug report, feature request, confusion, or even a question (for example, when someone asks for a feature which already exists, it may mean that I need to ensure the feature is better communicated or presented).

  1. Search for an existing copy of that request.
  2. If it does not already exist, create it in Tier 5, with the requester’s name attached to it (more on that below).
  3. If it does already exist, add the requester’s name to it. If it is in Tier 5, bump it to Tier 4.
  4. If there is a compelling enough reason for it to go in Tier 3 or higher, move it up and expand on it (sometimes it requires a little research, sometimes it requires some thinking or designing and getting feedback from requesters, and it always requires adding some notes).

When moving features that solve user problems into Tier 1, Tier 2, or Tier 3, it often should not just be a raw request, but a meta-thing that might combine multiple requests, and turn into a fuller concept. Requests become refined into concepts as they move over the Tier 4 to Tier 3 threshold.

Choosing what to work on

As with anything in the product development world, there is a science and an art to choosing directions.

Knocking out crashes and showstoppers always comes first. From there, the art of product direction plays a bit more of a role.

Ultimately, Tier 4 starts to show some patterns. These patterns can be turned into concepts that land in Tier 3 or Tier 2. But how do you pick what to work on in any given week?

At the start of every week, I ask myself these questions:

  1. Bolster the keys. Is there anything I need to work on this week to bolster the things people love about the app?
  2. Expand the keys and remove barriers. Is there anything I need to work on this week to expand into the things people need in order to love the app? Is there anything I need to work on this week to remove barriers that prevent people from loving the app?
  3. Directional development. Is there anything I need to work on this week to move the product in the direction I want to take it?

It’s up to your intuition as a product designer to know what directions your product should go in; which audiences it should seek to serve sooner, and which directions take the product closer to your vision, rather than further from it.

Trust your instinct here; you’re getting a lot of inputs in this requests list. Once informed by the list, step back from it and consider what your gut says — you’ll know when you feel good about the next step versus when you feel like you’re working on something for the wrong reasons. And if you realize you’ve started down the wrong direction, pause, seek to find out more information, and see where you are led.

Attach names to requests

I have every request organized in Things, with everyone who requested it listed in the title of the request itself. As I read down my list, I see a list of names. This is huge for my work for a number of reasons:

  1. When something ships, I can let the people who requested it know, and find out if I’ve truly solved their problem. I doubt most people love reading long changelogs. I publish them, but you don’t have to read them to find out that I shipped something you wanted. I’ll let you know personally. The difference this makes for people is big – they aren’t a ticket number; they know I’m serving their needs, and they have the opportunity to check out the change / fix / addition, and to let me know if it didn’t fully get at what they needed.
  2. I can track interest level. Not only can I see how many people requested something, I know who requested it. That means I can see if the only people who requested something don’t even use the app anymore, if their needs might have changed, etc.
  3. I can reach out for more info or with test builds. I do this a ton. Often I might start considering how to approach a certain item, and I have a decision to make that leaves me needing more information. With people’s names attached, I can reach out and find out more, or offer a proposed solution for feedback. I also often send over a test build to these folks, particularly when dealing with a hard-to-reproduce bug.
  4. I can consider what I already know about people’s use cases. I know who tends to have a clean inbox. I know who tends to have a full one. I know who tends to use lists a lot. I know who tends to have long threads. I know all of this from prior conversations with users. As I am approaching someone’s request or bug report, I know a lot of the context around the request already. This makes reproducing a bug significantly faster, or considering the ramifications of my decisions for the user much easier.
  5. I find a higher motivation to work on the items. With a name attached, I find myself with far greater motivation to move into an item. Instead of a big list of chores I need to tend to, on each request I can see exactly who will be excited by the item shipping; whose day will be made a little bit easier. The motivation this provides is awesome.

Ultimately, with a large audience, tagging requests with the names of those who made the request helps me to remember people and their needs, and to keep more in touch with them. A few times, users have mentioned their surprise when I remember something from interacting with them months prior, given how many others I communicate with in our community. It sounds small, but it helps everyone treat everyone else as the human behind the computer screen we all are, rather than just a faceless Slack handle or email address.

Give every request a short half-life

Every request should have a short half-life. That is, they decay in importance the longer they go without being requested again. There are many reasons for this: some feature requests are only wanted during the initial transition phase to an app. Some feature requests lose their relevance over time. Some feature requests get supplanted by new things. Some feature requests are from niches of users who find themselves better served by other solutions.

Basically: if I haven’t heard a request in a while, I drop it. It might not be relevant anymore. If it is relevant, either it’ll keep coming up (moving into Tier 4 or Tier 2) or I’ll consider it important to the direction of the product (moving into Tier 3).

So every now and then, usually after a big release (every few weeks), I go through the list, clearing out things which have been served by an update, and clearing out things which I haven’t heard about in a while (into an archive for reference). It doesn’t mean the request is unimportant, it just means that a request with three votes from this week is more important than a request with six votes if the last vote was made many months ago. A system which enforces this half-life is necessary to progressing in directions flexibly informed by all the information you have today, rather than letting information relevant to some day many months ago put present information in the shadows.

Why the list exists

As I mentioned, the most important feature requests will keep coming up. Because of this, I used to not write down feature requests at all. I allowed the ones that kept coming up to be what I worked on.

But this drastic of an approach was problematic: I found that simply allowed the angriest or loudest requesters to dictate too much of our development, which isn’t a good way to go (angry and loud requesters are, individually, only around for a short time).

Having requests tracked keeps me focused on the things being genuinely requested a bunch. Though, as I mentioned, it does require that I let things drop off if they seem to have lost support.

Don’t publish your list

People regularly ask me to publish a development schedule so they don’t have to ‘bother’ me with their questions or requests.

Here’s why I don’t do this: besides the simple fact that I don’t know how long a certain fix or feature might take, when a list is published, requests for the things on that list disappear. People silently wait. I no longer know which of those things is most important. This is why I don’t publish a big development list of every item coming soon.

Instead, I get to hear directly from customers every day about the things most important to them. This allows me to make development decisions informed by the latest information every single day.

It also gives me the opportunity to connect with each person making a request, and often allows me to discover new details or nuances about various requests as people share their own take on the request.

These are the things that make it possible for me to be effective with Mail Pilot’s volume of user requests.

Want new articles delivered straight to you? Subscribe here.

Managing many user requests at once with Mail Pilot is an entire system in itself. This essay breaks down the organization and process I use to hande and make use of a high volume of requests.

Email is a very personal thing. Everyone has their own unique list of “must-have” features for an email client. Managing many user requests at once is a skill that I have become proficient at, but only after years of gracelessly fumbling through poor processes.

Here are the organization, process, and other details that have served me well for the last several years as I’ve handled support alone.

Read the article →
Introducing: The Card Style UI
by Alexander Obenauer
in
Announcements
published
April 14, 2020
by Alexander Obenauer
in
Announcements
published
April 14, 2020

In March, we introduced The New Mail Pilot to the world. Missed the announcement? Check it out here »

One of the first things you'll notice about the new app is its entirely new user interface.

The new card-style user interface brings your most important emails to your fingertips, allowing you to fluidly and directly interact with your inbox, without having to navigate around a bunch of screens to do what you need to do. Check out this new video all about it:

After years of design and development, we have spent the last six months testing and tweaking this new interface with a group of dedicated testers who have helped us refine this new vision for email.

Now, we're beginning to roll it out. Stay tuned for more video updates, be sure to subscribe to our YouTube channel, and make sure you're receiving our newsletter by subscribing on the home page.

Introducing: The New Mail Pilot
by Alexander Obenauer
in
Announcements
published
April 14, 2020
by Alexander Obenauer
in
Announcements
published
April 14, 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
April 14, 2020
by Alexander Obenauer
in
Learn to Code
published
April 14, 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
April 14, 2020
by Alexander Obenauer
in
Announcements
published
April 14, 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