A place to cache linked articles (think custom and personal wayback machine)
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

index.html 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang="fr">
  4. <!-- Has to be within the first 1024 bytes, hence before the `title` element
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset="utf-8">
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name="viewport" content="width=device-width,initial-scale=1">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>Retiring Pinafore (archive) — David Larlet</title>
  13. <meta name="description" content="Publication mise en cache pour en conserver une trace.">
  14. <!-- That good ol' feed, subscribe :). -->
  15. <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
  16. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  17. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
  18. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
  19. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
  20. <link rel="manifest" href="/static/david/icons2/site.webmanifest">
  21. <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
  22. <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
  23. <meta name="msapplication-TileColor" content="#f7f7f7">
  24. <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
  25. <meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
  26. <meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
  27. <!-- Documented, feel free to shoot an email. -->
  28. <link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
  29. <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
  30. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  31. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  32. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  33. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  34. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  35. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  36. <script>
  37. function toggleTheme(themeName) {
  38. document.documentElement.classList.toggle(
  39. 'forced-dark',
  40. themeName === 'dark'
  41. )
  42. document.documentElement.classList.toggle(
  43. 'forced-light',
  44. themeName === 'light'
  45. )
  46. }
  47. const selectedTheme = localStorage.getItem('theme')
  48. if (selectedTheme !== 'undefined') {
  49. toggleTheme(selectedTheme)
  50. }
  51. </script>
  52. <meta name="robots" content="noindex, nofollow">
  53. <meta content="origin-when-cross-origin" name="referrer">
  54. <!-- Canonical URL for SEO purposes -->
  55. <link rel="canonical" href="https://nolanlawson.com/2023/01/09/retiring-pinafore/">
  56. <body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">
  57. <article>
  58. <header>
  59. <h1>Retiring Pinafore</h1>
  60. </header>
  61. <nav>
  62. <p class="center">
  63. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  64. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  65. </svg> Accueil</a> •
  66. <a href="https://nolanlawson.com/2023/01/09/retiring-pinafore/" title="Lien vers le contenu original">Source originale</a>
  67. </p>
  68. </nav>
  69. <hr>
  70. <p>Five years ago, I started <a href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">a journey</a> to build a better Mastodon client – one focused on performance and simplicity. And I did! Pinafore is the main Mastodon client I’ve used myself since I first released it.</p>
  71. <p>After five years, though, my relationship with social media <a href="https://nolanlawson.com/2022/11/22/thoughts-on-mastodon/">has changed</a>, and it’s time for me to put Pinafore out to pasture. The <a href="https://pinafore.social">pinafore.social</a> website will still work, but I’ve marked <a href="https://github.com/nolanlawson/pinafore">the repo</a> as unmaintained.</p>
  72. <h2>Why retire Pinafore?</h2>
  73. <p>I don’t have the energy to do this anymore. Pinafore has gone from being a fun side project to being a source of dread for me. There is a constant stream of bug reports, feature requests, and pull requests to manage, and I just don’t want to spend my free time doing this anymore.</p>
  74. <p>By the way, this is not my first rodeo. Read <a href="https://nolanlawson.com/2017/03/05/what-it-feels-like-to-be-an-open-source-maintainer/">this post</a> on my breakup with another open-source project.</p>
  75. <h2>Why not pass it off to a new maintainer?</h2>
  76. <p>Running a fediverse client requires trust. People who use Pinafore are trusting me to handle their data securely. As such, I’ve been meticulous about using <a href="https://observatory.mozilla.org/analyze/pinafore.social">good security headers</a> and making <a href="https://github.com/nolanlawson/pinafore/issues/139">pro-privacy decisions</a>. A new maintainer (through malice or ignorance) could add new functionality that compromises on security or privacy, essentially trading on my good name while harming users.</p>
  77. <p>Over the years, I have had <a href="https://github.com/nolanlawson/pinafore/issues/1886">lots</a> <a href="https://github.com/nolanlawson/pinafore/issues/2339">of</a> <a href="https://github.com/nolanlawson/pinafore/issues/2261">feature</a> <a href="https://github.com/nolanlawson/pinafore/issues/1671">requests</a> that would inadvertently cause a privacy or security leak, and I’ve pushed back on every single one. (E.g. “Why not contact third-party servers to show the full favorite/boost count?” Well, because users may trust their home server, but that doesn’t mean they trust random third-party servers.)</p>
  78. <p>Rather than trust that a new maintainer will keep these high standards in place, I’d rather put Pinafore in a frozen state.</p>
  79. <h2>Why not shut it down entirely?</h2>
  80. <p>Thanks to <a href="https://vercel.com">Vercel’s</a> generous free tier, Pinafore costs me $0 per month to run. It’s just static HTML/CSS/JS files, after all.</p>
  81. <h2>Why are you the sole maintainer?</h2>
  82. <p>I’m not – there have been <a href="https://github.com/nolanlawson/pinafore/graphs/contributors">tons</a> of contributions through the years. But for the most part, these have been “drive-by” in nature (nothing wrong with that!), rather than someone deeply learning the codebase end-to-end.</p>
  83. <p>I suspect one of the reasons for this is that Pinafore is written in Svelte v2 and <a href="https://sapper.svelte.dev/">Sapper</a> – both of which are deprecated in favor of Svelte v3 and <a href="https://kit.svelte.dev/">SvelteKit</a>. Not only is there <a href="https://github.com/sveltejs/svelte/issues/2462">no migration path</a> from Svelte v2 to v3, but <a href="https://kit.svelte.dev/docs/migrating">there isn’t one</a> from Sapper to SvelteKit either. (And on top of that, I had to <a href="https://github.com/nolanlawson/sapper">fork Sapper</a> pretty heavily.) Anyone making a bet on learning Pinafore’s tech stack is investing in a dead framework, so it’s not very attractive for new maintainers.</p>
  84. <p>So why didn’t I bother updating it? Well, it’s a lot of work to manually migrate 200+ components to what is essentially a new framework. And plus, as far as I could tell, it would be a pure DX (Developer Experience) improvement, not a UX (User Experience) improvement. (I just wouldn’t be using any of SvelteKit’s new features, and Svelte v3 doesn’t seem to have massive UX improvements over Svelte v2.)</p>
  85. <h2>What did you learn while writing Pinafore?</h2>
  86. <p>Now here’s an interesting question! And one that may be useful for those building their own Mastodon (or fediverse) clients. It is my sincerest wish that Pinafore inspires other developers to build their own (and better!) clients.</p>
  87. <h3>API and offline</h3>
  88. <p>First off, <a href="https://en.wikipedia.org/wiki/ActivityPub">ActivityPub</a> does have a <a href="https://www.w3.org/TR/activitypub/#client-to-server-interactions">client-to-server API</a>, but as far as I can tell, it’s not really worth implementing. Mastodon is the 800-pound gorilla in the fediverse, <a href="https://github.com/mastodon/mastodon/issues/10520">it doesn’t implement</a> this API, and other servers (such as <a href="https://pleroma.social/">Pleroma</a> and <a href="https://github.com/misskey-dev/misskey">Misskey</a>) implement their own flavor of Mastodon’s API. And plus, <a href="https://docs.joinmastodon.org/methods/">Mastodon’s REST API</a> is pretty sensible and doesn’t change too frequently. (And when it does, they add a <code>/v2</code> endpoint while still maintaining the <code>/v1</code> version.)</p>
  89. <p>However, the fact that Mastodon has a fairly bog-standard REST API makes it pretty difficult to implement offline support, as I did in Pinafore. Essentially, I implemented a <a href="https://github.com/nolanlawson/pinafore/tree/ed9a9f6539066159801e7df40a6be2f37a8c5588/src/routes/_database">full mirror</a> of Mastodon’s PostgreSQL database structure, but on top of <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a>. On top of that, I had to implement a variety of strategies to synchronize data between the client and server:</p>
  90. <ul>
  91. <li>As new statuses stream in, how do you backfill ones you may have missed if the user went offline? Well, you have to just keep <a href="https://github.com/nolanlawson/pinafore/blob/ff53fcab10d01c02a762a824d35a961473991668/src/routes/_actions/stream/fillStreamingGap.js#L9">fetching statuses to fill the gap</a>.</li>
  92. <li>How do you deal with deleted statuses? Well, you have to remove them from the in-memory store, and the database, and then also go ahead and delete any statuses that boosted them or notifications that reference them… <a href="https://github.com/nolanlawson/pinafore/blob/ff53fcab10d01c02a762a824d35a961473991668/src/routes/_actions/deleteStatuses.js">It’s a lot</a>. (And don’t get me started on <a href="https://github.com/nolanlawson/pinafore/issues/2179">editing statuses</a>! I didn’t even get around to that.)</li>
  93. <li>How to deal with slow servers? Well, you can implement <a href="https://github.com/nolanlawson/pinafore/blob/ff53fcab10d01c02a762a824d35a961473991668/src/routes/_components/status/StatusToolbar.html#L121-L131">an optimistic UI</a> that shows (for instance) a “favorited” animation while still waiting for the server to respond. (And also cancels if the server responds with an error or times out.)</li>
  94. </ul>
  95. <p>From my years working on <a href="https://pouchdb.com">PouchDB</a>, I know that it’s a fool’s errand to try to implement proper client-server synchronization without a holistic plan for managing revisions, conflicts, and offline states… and yet, I did it. The end result is pretty impressive in my opinion, even if arguably it doesn’t add a lot to the user experience. (There’s not much you can do in a social media app when you’re offline, and I’m sure people still frequently have to refresh when stuff gets out-of-date.)</p>
  96. <h3>Performance</h3>
  97. <p>Speaking of which, refreshes should be fast! And I believe Pinafore is pretty good at this. (I can’t find the link, but someone did a recent analysis showing that Pinafore uses less CPU and memory than the default Mastodon frontend.)</p>
  98. <p>In short, I’d say it’s entirely possible to build a performant SPA (despite some of my <a href="https://nolanlawson.com/2022/05/21/the-balance-has-shifted-away-from-spas/">misgivings about SPAs</a>). But it helps if:</p>
  99. <ul>
  100. <li>You have a browser perf background (like me).</li>
  101. <li>You’re only one developer. (Much harder to implement tricky perf optimizations if you have to explain it to your colleagues!)</li>
  102. <li>You use a perf-focused framework like <a href="https://svelte.dev">Svelte</a>.</li>
  103. <li>You don’t do much! Pinafore has a fraction of the features of the main Mastodon frontend.</li>
  104. <li>You’re merciless about removing dependencies, or <a href="https://github.com/nolanlawson/emoji-picker-element/">writing your own dependencies</a> when the existing ones are too slow or bloated.</li>
  105. <li>You’re meticulous about little micro-optimizations (e.g. <a href="https://nolanlawson.com/2021/08/08/improving-responsiveness-in-text-inputs/">debouncing</a>, <a href="https://github.com/sveltejs/svelte/issues/1098">event delegation</a>, or <a href="https://github.com/nolanlawson/pinafore/blob/ff53fcab10d01c02a762a824d35a961473991668/docs/Architecture.md#every-sapper-page-is-duplicated">page splitting</a>) that improve the user experience, especially on low-end devices, but make the developer experience a lot worse.</li>
  106. </ul>
  107. <p>Not all of this is necessary to make a fast, fluid API, but it certainly helps. And the fact that I ended up building something that <a href="https://nolanlawson.com/2019/09/22/the-joy-and-challenge-of-developing-for-kaios/">can run on feature phones</a> gives me a lot of satisfaction.</p>
  108. <h3>Accessibility</h3>
  109. <p>I didn’t set out to write “the accessible Mastodon client,” but I’ve heard from a lot of folks that Pinafore is one of the better ones out there, especially for blind users.</p>
  110. <p>For this, I mostly have to thank <a href="https://www.marcozehe.de/">Marco Zehe</a> and <a href="https://www.jantrid.net/">James Teh</a> (among others), who provided tons of feedback and really helped with the polish of the screen reader experience. Accessibility <a href="https://nolanlawson.com/2019/11/05/what-ive-learned-about-accessibility-in-spas/">isn’t always black-and-white</a> – like anything in design, sometimes there are tradeoffs and differing opinions on what the best option is. Leaning on the expertise of actual blind users gave me insights that I couldn’t have had otherwise.</p>
  111. <p>Another thing that helps is just giving a damn. When I started on Pinafore, I didn’t really know much about accessibility, but I decided it was time to finally learn. I started off with <a href="https://youtube.com/watch?v=5R-6WvAihms">a basic intro to screen readers from Rob Dodson</a>, played around with VoiceOver and NVDA, and tried to read and understand as much as I could. I wouldn’t call myself an accessibility expert, but I’ve made a lot of progress in the past five years, and now I wince when I look back at some of the code I wrote in the past.</p>
  112. <p>In the end, I found accessibility to be quite rewarding. Rather than feeling like a chore or a box-ticking exercise, it feels like a fun challenge. In some cases it’s just about leaning on existing web standards, but in other cases it feels like you’re building a parallel semantic UI to the visual one. Sometimes I found that this even <a href="https://nolanlawson.com/2020/07/01/building-an-accessible-emoji-picker/">influenced the overall architecture</a> of my code – which goes to show that it’s better to consider accessibility upfront rather than as an afterthought.</p>
  113. <p>That said, I definitely messed up some stuff when it comes to accessibility – <a href="https://webaim.org/articles/contrast/">color contrast</a> in particular is something I did a poor job on. (Luckily <a href="https://nickcolley.co.uk/">Nick Colley</a> has put a bunch of work into Pinafore to improve this!)</p>
  114. <h2>Conclusion</h2>
  115. <p>Pinafore was a fun project. I learned a lot about web development while working on it. Often, when a new feature landed in browsers – e.g. <a href="https://github.com/nolanlawson/pinafore/pull/2088"><code>color-scheme</code></a>, <a href="https://github.com/nolanlawson/pinafore/pull/1857">maskable icons</a>, or <a href="https://github.com/nolanlawson/pinafore/blob/ff53fcab10d01c02a762a824d35a961473991668/src/routes/_utils/polyfills/loadPolyfills.js#L9-L23">various <code>Intl</code> APIs</a> – I would eagerly integrate it into Pinafore, which helped me learn more about how browsers work.</p>
  116. <p>In another case, I went <a href="https://nolanlawson.com/2020/06/28/introducing-emoji-picker-element-a-memory-efficient-emoji-picker-for-the-web/">a bit overboard</a> on building my own emoji picker for Pinafore, and in the process learned <a href="https://nolanlawson.com/2022/04/08/the-struggle-of-using-native-emoji-on-the-web/">way more than I ever wanted to know</a> about fonts and emoji rendering.</p>
  117. <p>I also think that Pinafore accomplished many of the goals I had in mind when I originally wrote it. At the time, Mastodon only had a multi-column UI, which many users found overwhelming and confusing. Pinafore demonstrated that a single-column layout could be a viable alternative, and since then, Mastodon has defaulted to <a href="https://blog.joinmastodon.org/2019/06/mastodon-2.9/">a single-column layout</a>.</p>
  118. <p>Back then, there was also only one web-based Mastodon client (<a href="https://halcyon.mstdn.social/">Halcyon</a>), and it didn’t support logging in to more than one instance at a time. Pinafore proved it was possible for a web-based client to do this (not obvious given <a href="https://developer.mozilla.org/en-US/docs/Glossary/CORS">CORS</a> constraints!), and nowadays there are lots of web-based clients, such as <a href="https://nicolasconstant.github.io/sengi/">Sengi</a>, <a href="https://www.cuckoo.social/">Cuckoo+</a>, and <a href="https://elk.zone">Elk</a>, and many of them support multi-instance logins.</p>
  119. <p>Pinafore isn’t going anywhere – like I mentioned, the site is still up and running. I also think it could serve as an interesting point of comparison for other Mastodon clients. (Try to beat Pinafore on performance and accessibility! I think that would be a great outcome.)</p>
  120. <p>I also want to thank everyone who followed along with me on this journey over the years, and who either used Pinafore, filed a bug, or contributed to it. Thank you for giving me one of my career-defining projects over the last half-decade. It wouldn’t have been possible without your help.</p>
  121. </article>
  122. <hr>
  123. <footer>
  124. <p>
  125. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  126. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  127. </svg> Accueil</a> •
  128. <a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
  129. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
  130. </svg> Suivre</a> •
  131. <a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
  132. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
  133. </svg> Pro</a> •
  134. <a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
  135. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
  136. </svg> Email</a> •
  137. <abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
  138. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
  139. </svg> Légal</abbr>
  140. </p>
  141. <template id="theme-selector">
  142. <form>
  143. <fieldset>
  144. <legend><svg class="icon icon-brightness-contrast">
  145. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
  146. </svg> Thème</legend>
  147. <label>
  148. <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
  149. </label>
  150. <label>
  151. <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
  152. </label>
  153. <label>
  154. <input type="radio" value="light" name="chosen-color-scheme"> Clair
  155. </label>
  156. </fieldset>
  157. </form>
  158. </template>
  159. </footer>
  160. <script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
  161. <script>
  162. function loadThemeForm(templateName) {
  163. const themeSelectorTemplate = document.querySelector(templateName)
  164. const form = themeSelectorTemplate.content.firstElementChild
  165. themeSelectorTemplate.replaceWith(form)
  166. form.addEventListener('change', (e) => {
  167. const chosenColorScheme = e.target.value
  168. localStorage.setItem('theme', chosenColorScheme)
  169. toggleTheme(chosenColorScheme)
  170. })
  171. const selectedTheme = localStorage.getItem('theme')
  172. if (selectedTheme && selectedTheme !== 'undefined') {
  173. form.querySelector(`[value="${selectedTheme}"]`).checked = true
  174. }
  175. }
  176. const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
  177. window.addEventListener('load', () => {
  178. let hasDarkRules = false
  179. for (const styleSheet of Array.from(document.styleSheets)) {
  180. let mediaRules = []
  181. for (const cssRule of styleSheet.cssRules) {
  182. if (cssRule.type !== CSSRule.MEDIA_RULE) {
  183. continue
  184. }
  185. // WARNING: Safari does not have/supports `conditionText`.
  186. if (cssRule.conditionText) {
  187. if (cssRule.conditionText !== prefersColorSchemeDark) {
  188. continue
  189. }
  190. } else {
  191. if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
  192. continue
  193. }
  194. }
  195. mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
  196. }
  197. // WARNING: do not try to insert a Rule to a styleSheet you are
  198. // currently iterating on, otherwise the browser will be stuck
  199. // in a infinite loop…
  200. for (const mediaRule of mediaRules) {
  201. styleSheet.insertRule(mediaRule.cssText)
  202. hasDarkRules = true
  203. }
  204. }
  205. if (hasDarkRules) {
  206. loadThemeForm('#theme-selector')
  207. }
  208. })
  209. </script>
  210. </body>
  211. </html>