An exciting iOS announcement, recent updates, and a Black Friday Pro discount! (40% off first year, new subs)

Bundled Notes
7 min readNov 25, 2022

--

Let me jump right into the exciting news: a native iOS Bundled Notes app is being actively built. And even more exciting: it’s not being built — at least primarily — by me!

Keen Bundled users already know that the app has been a solo-dev project, with the significant exception of the awesome community translation efforts, moderation on community channels (thanks Maciek 😊) and the very active testing community on Telegram — but this is finally changing!

Over the last two weeks, I have started working with an experienced iOS developer, Max Chiesa, on the iOS project. It will be entirely native, built with iOS design patterns in mind, and open sourced before the beta release.

I’ll have *lots* to share on this in the coming weeks, but here’s a cheeky preview of the design in the meantime:

The last 8 months

When Bundled was first released — as an Android-only app with rudimentary note-taking features — it was a cosy little home. It was easy to keep clean, easy to extend. Then came the first extension, a web app, and the home doubled in size. A highly-requested pool (reminders) was next, followed by a full re-fitting (attachments). And don’t forget the 100s of tweaks along the way — a guest bedroom, better fridge, a new TV and in-built heating.

The point of this lame analogy is that the metaphorical Bundled Notes house is now very big, and it’s getting hard just to keep every room clean, let alone to build more (iOS, collaborative bundles, importing, global search, full-E2EE).

With so much Bundled housework needed and the legacy of some poor architectural designs slowing me down, most of the effort over the last 6 months has been spent on changes that are pivotal for speeding up a future feature development, but that are essentially invisible to users. Here’s a short list of examples:

  • Built a proper iOS/iPadOS proof-of-concept with SwiftUI
  • Set up CI/CD for the Web app via GitHub Actions
  • Set up CI for the Android app via GitHub Actions
  • Split the Android text-editor and Common off to a seperate project for future open-sourcing/improved project structure
  • Built an internal Forms library to easily build pretty dialogs on the Android and Web app
  • A huge web-app refactor to break up a poorly-designed “god” component
  • Completed migration of web app theming from SCSS files to EmotionCSS to improve theme consistency and support Material You
  • Rebuilt the link preview generation endpoint and significantly improved rich preview generation stability
  • Refactored server-side code for better code structure/stricter Typescript rules
  • Rewrote the note list rendering on Android so I could re-use it across the app (a pre-requisite for global search functionality)
  • Finished a full TypeScript refactor on web, which has markedly improved stability
  • Set up third-party support software to ensure I can keep on top of support requests
  • Built a strategy for continuing the operation of the app in case of personal emergency
  • Upgraded to new Billing API to support subscription discounts and offers

Now that I’ll be working with an experienced iOS developer, a huge weight is off my shoulders — not only from the perspective of time, but mental stress. It was becoming difficult to imagine and organise the work I had to do with iOS in the mix — and, at times, I found myself making excuses not to sit down for Bundled work given the sheer amount that needed doing.

Notably, the focus on tech-debt and my being slightly overwhelmed has lead to the delay of plans I was hoping to realise by this time of the year—releasing at least one milestone feature, writing more articles, increasing community presence via personal videos/demos, and releasing feature voting. Alas.

Looking forward

For now, I am returning my focus to the Android and Web apps — and they need it. The Android app is begging for huge refactors, and the web app is still not at “basic feature” parity (i.e. anything marked as ‘Available’ on Android, but ‘Planned’ for Web here: Features). I want to be diligent about communicating the work I’ve done (even if it is invisible or boring) at least once a month, not only as a way of increasing community presence, but also to personally feel a sense of continuous progress.

I also intend to improve collaboration beyond the iOS work, paying for graphics & assets to be made and added to all versions of the app. Next year, I will be considering what a piecemeal “green-field” Android refresh would look like.

Immediately, my goal is to finish the massive web app refactor, and Android app refactors and QoL improvements. When I have made significant progress on both of the above, I will be pivoting to a new cross-platform milestone feature: global search. Client-side encryption — the last piece of the puzzle for E2E encryption — will be the next milestone feature after that.

Nerd stuff

The web app refactor I’m currently working on is probably the most substantial code uplift I’ve undertaken on either the web or Android Bundled app.

If you’re not familiar with React/Context/Hooks, the short version of the work is that I completely rebuilt the code for the entire bundle page (where you see entries, filter tags, search etc.) — converting a 1430 line “god” file into a 300 line file, and breaking off all the re-usable logic in that original file into re-usable parts.

For example, the code for loading and sorting notes, rendering a tag filter list, and filtering entries by tags is now totally re-usable — this meant I was able to add a search bar and tag filtering to the archived notes dialog in seconds.

For those familiar with React/Hooks and who love code snippets, the long version of this story is that I rebuilt the Bundle page by breaking off every re-usable bit of logic into automatically updating hooks. This is how easy it is to load entries & tags in any component I write on Bundled Web:

const { bundle } = useBundle(bundleId)
const { isLoading, entries, sortedTags } = useEntries({ bundle })

If you change the bundle sort order, the bundle variable instantly updates to reflect the change, and the useEntries() hook detects the updated bundle and automatically re-sorts and returns an updated entries list.

useEntries also supports criteria, filters, search queries and even the ability to overwrite the sort method. This is how it is used on the archive list dialog:

const { isLoading, sortedTags, entries } = useEntries({
bundle,
entriesFilter: EntryFilter.ARCHIVED_ONLY,
sortMethod: archivedDateComparator,
filterTagsState: tagSelectionState,
query,
})

But it doesn’t end there! The logic for Kanban boards used to be a real pain in the ass. I rewrote it from scratch and moved it into a hook. Now, building Kanban columns on the Bundle page is this simple:

const { columns, kanbanEnabled } = useKanban({
bundle,
entries: entries,
tags: sortedTags,
hashedTags: hashedTags,
})

And the multi-select functionality has been written from scratch, made into a generic hook that will be added to other parts of the web app soon, like the archive dialog and attachments menus:

const multiSelector = useMultiSelect({
listId: bundle.id,
supplyAllIds: () => entries.map(entry => entry.id),
})

...
// in entry menu:
multiSelector.toggleSelection(entry.id)

// in entry view
multiSelector.isSelected(entry.id)
...

// multiselector features:
export type MultiSelector = {
selectedIds: Record<any, any>
toggleSelection: (id: any) => void
isSelected: (id: any) => boolean
clearSelection: () => void
selectAll: () => void
selectionActive: boolean
selectionCount: number
}

And I’ve created a new hook for database management which uses JS destructuring and TypeScript Partial<> to make updating a Bundle incredibly simple:

const { updateBundle } = useBundledFirestore()

...
onChecked={checked => updateBundle(bundle.id, { compactTags: checked })}
...

Finally, because I was on a built of a roll with hooks, I built two more, replacing legacy methods of loading data for various components:

const { bundles, isLoading } = useBundles() // use for bundle sidebar list
const { sortedTags } = useTags({ bundleId }) // used for text editor

While I was rebuilding all the above, I took the opportunity to add support for features which were missing on the web. For example, the Android app has had the ability to change the filter behaviour of tags (i.e. if you select two tags, do you show every entry with both selected tags, or all entries with either selected tag). This has been added in useEntries(), and now works on the bundle page.

Even better, rewriting all this processing and fetching logic has lead to notable performance improvements. Bundles load quicker, and their settings reflect in entry lists essentially instantaneously, where it used to take roughly a second.

This is what it looks like when all the above rewrites come together (and to put it into perspective, every new feature you see in the below screenshot, the tags lists, filtering behaviours and search bar, only added 10 lines to the ArchiveListDialog.tsx file):

Anyway, that wraps the dev summary! Please let me know if the above was interesting, I might start sharing snippets and tutorials more if people like them. 😊

Thanks for reading!

**Sale!

There is also a Black Friday sale! 40% off Pro for the first year of a yearly subscription, and 20% off for the first year of a monthly subscription. This is only available to users who have not subscribed before!

It’ll be up for a week, so no major hurry!

--

--