I was on the mastodon.technology Mastodon server for the last several months, but due to the admin’s personal situation he had to shut down the server, so I decided to try setting up my one single-user instance. (It actually turned out to be a tiny multi-person instance when my friend Mike asked to join.)

(I’m @json@micro.sadlerjw.com. Give me a follow!)

I tried to get everything set up using Docker Compose based on the docker-compose.yml file in the Mastodon repo. It was pretty straightforward, but I ran into a few gotchas, which I figured I’d document here:

  1. The Mastodon services expect the Postgres user to be a member of a group named with the same name. (If your DB user is mastodon, then its group should also be mastodon.
  2. The Mastodon web/API server will silently refuse any non-HTTPS connections. If you’re running a reverse proxy in front of it which terminates the SSL connection, you need to include the X-Forwarded-Proto header. This was really hard to figure out, since the Mastodon server didn’t so much as log the connection attempt.
  3. The Mastodon web/API server will reject any connection not using the hostname it was configured to run under. It does log that it rejected the connection, but not the reason.
  4. You may want your Mastodon handle (eg @json@micro.sadlerjw.com) to use a domain that’s different from the one the server runs on. For instance, my server is at https://micro.sadlerjw.com. If I had known how, I would have configured my handle to be @json, without the “micro”. You can do this using the LOCAL_DOMAIN and WEB_DOMAIN environment variables. You can find details in the documentation. You can’t change this after you start using your instance!
  5. SendGrid has a free tier if you send less than 100 emails per day. This post on the Mastodon Discourse forums helped me get it set up.

There are definitely a few minor things that don’t seem to work properly, but I’m not sure if it’s either “federation ain’t perfect,” or something that’s my fault, or a problem with the software. For instance, I can’t seem to add a post with video uploaded from my iPhone. Your mileage may vary.

Hope this can help someone! At a minimum, this has been a fun experiment. I definitely wouldn’t recommend administrating a server for a large community though…that sounds like a lot of work both in terms of setting policies and moderation, and also in terms of keeping the services up and running and responding quickly. But stand up a little server for your friends, it might be fun!


Psychonauts 2

★★★★☆

I finished playing Psychonauts 2 the other night. What a great game. Cute and funny writing, incredibly clever and varied gameplay, a solid (if heavily foreshadowed) story, and great voice acting (including Elijah Wood)!

The game has decent character diversity, featuring a disabled character, a gay couple, and characters dealing with all sorts of mental difficulties. The game focuses on the founders of The Psychonauts, who experienced a traumatic event together long ago, and has the player character, Raz, jumping into their brains to help them work through their responses to that event. Its treatment is light-hearted but not trivializing (all of the characters are treated with a lot of empathy), and the physical manifestations of these characters’ minds is absolutely stunning in their creativity. The level design is off the charts.

In spite of the game’s inclusiveness, it has some blind spots. Its use of Eastern European stereotypes throughout is uncomfortable, and the treatment of the inside of an East Asian character’s mind in one of the later levels is more than a little problematic. If you make a joke about how smelly a fish market is, you’ve made a joke about fishes; but if you make a joke about how dirty a fish market is in this context, as Phsychonauts 2 does, you’ve made a joke about the sanitariness of an East Asian culture. It reminds me a bit of the term “wet markets” and the demonization of those Chinese markets in the early stages of the pandemic. The level only has a few missteps like this, but better to have avoided them altogether.

The story was very emotionally engaging in the first 80% of the game, but unfortunately the ending fell a little bit flat for me, not living up to what I think could have been something really, really special. After the final battle, it just sort of…ended.

In spite of some cultural missteps, the rest of the game is truly wonderful. If you feel like giving it a try and you have Xbox Game Pass, it’s available there!


TLDR: Table view (and collection view) data source should be backed by a snapshot of your data store; not a real-time view of it.

I attended last week’s iOS KW meetup, which was a talk by Chris Liscio about Collection View Controllers on iOS and macOS. The talk was good, featuring a lot of interesting details of how Chris implemented the main view in his music app, Capo. But what was really interesting is what I learned in chatting with him afterwards. I’ve been doing iOS development for more than 6 years, and this finally flipped a switch in my brain about why managing updates in table views (and collection views) was always so difficult for me.

Animating updates to a UITableView’s contents is ostensibly pretty easy—you get notifications about the contents of your data store, and you tell your table view which rows were added, removed, or updated. In order to have things animate all together nicely, you do this in a transaction:

tableView.performBatchUpdates { 
	tableView.insertRows(at: [IndexPath(row: 3, section: 1)], animation: .automatic) 
	tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], animation: .automatic)
}

However, there are some arcane rules about the order in which these updates are processed—not in the order in which you submit them, but all reloads first, then deletes, then insertions. In other words, reloads and deletes are processed relative to the state of the table before you begin making any updates, and insertions are processed relative to the state of the table once deletions have completed.

In the past, when I had to make updates based on a completed network request, for instance, I would call tableView.beginUpdates(), then respond to KVO notifications by calling insertRows() and its friends as the network response handler added, updated, and removed items in the data store, and then call endUpdates() when processing was complete. Since the updates to the data store could happen in any order, it was extremely difficult to try to re-order everything so that it would make sense in the order that UITableView performs its updates. Crashes occurred often. This was also a source of high coupling between classes that should have little or now knowledge of each other.

It turns out the answer is very simple. As Chris described, your data source should represent a snapshot of your data, not a real-time view of it. After changes have finished, you move your snapshot forward, compute a diff, and send the necessary insert / delete / refresh messages to the table view. This is apparently what NSFetchedResultsController does under the covers, and other technologies such as Realm allow you to either update your thread’s snapshot manually or at the beginning of a runloop.

The key is that any changes that occur in your UITableViewDataSource should only happen during a table view updates transaction. If your data source reflects the real-time state of your backing store, values might have already changed by the time you’re responding to a them, which can get you into trouble with the frameworks.

So, lesson learned, 6+ years later: `UITableViewDataSource` shouldn’t represent live data. Move your snapshots forward during your table view’s update transactions. Have fewer crashes.


The Sublime Text folks made a git client.

Today, I’d like to introduce Sublime Merge. It combines the UI engine of Sublime Text, with a from-scratch implementation of Git. The result is, to us at least, something pretty special.

Oh wow. Oh wow.

We have a custom implementation of Git for reading repositories, which drives a lot of our high performance functionality. However we defer to Git itself for operations that mutate the repository (Staging, Committing, Checking out branches, etc).

Sign me up!


Apple has differentiated its new iPhones in a super frustrating way, probably in an effort to drive up average selling prices.

  • iPhone XR: phenomenal performance to price ratio. Offers 128 GB storage. Awesome colour choices. Aluminum instead of steel. Inferior but still great screen. No 3D Touch. Only one camera.
  • iPhone XS and XS Max: awesome screen. Two cameras. Doesn’t offer 128 GB – only 64, 256, and 512.

I am currently using 78 GB of storage on my iPhone 7+. Way too low to justify 256 GB for an additional $210, but way too high to try to fit it all in a 64 GB phone. And I really want the optical zoom capability of the dual camera setup – something I use all the time on my current phone.

So either I can pay $1099 and settle for a single camera, or pay a whopping $1589 to get all the things I actually want, plus a bunch of things I don’t care about.

I’m willing to bet a lot of other people are in the same camp.

Furthermore, Apple is selling these new devices at CAD$1.40 to the US Dollar, instead of the actual current exchange rate, CAD$1.30. They usually treat us Canadians pretty fairly, but this is awful.