Skip to content
cheeaunPublic

About

A minimalistic opinionated Mastodon web client

Topics

Resources

Security policy

Stars

1.5k stars

Watchers

8 watching

Forks

Latest commit

 

History

4,909 Commits

Folders and files

Repository files navigation

Phanpy

Minimalistic opinionated Mastodon web client.

Fancy screenshot

🗣️ Pronunciation: /fænpi/ (FAN-pee) 🔊 Listen

This is an alternative web client for Mastodon.

  • 🏢 Production: https://phanpy.social Uptime Robot status
    • GitHub Release
    • production branch
    • break less often
    • slower fixes unless critical
  • 🏗️ Development: https://dev.phanpy.social Uptime Robot status
    • GitHub last commit (branch)
    • main branch
    • may see new cool stuff sooner
    • may break more often
    • may be fixed much faster too

🐘 Follow @phanpy on Mastodon for updates ✨

Everything is designed and engineered following my taste and vision. This is a personal side project for me to learn about Mastodon and experiment with new UI/UX ideas.

Features

  • 👪 Multiple accounts
  • 🪟 Compose window pop-out/in
  • 🌗 Light/dark/auto theme
  • 🔔 Grouped notifications
  • 🪺 Nested comments thread
  • 📬 Unsent draft recovery
  • 🎠 Boosts Carousel™️
  • ⚡ Shortcuts™️ with view modes like multi-column or tab bar
  • #️⃣ Multi-hashtag timeline

Design decisions

  • Status actions (reply, boost, favourite, bookmark, etc) are hidden by default.
    They only appear in individual status page. This is to reduce clutter and distraction. It may result in lower engagement, but we're not chasing numbers here.
  • Boost is represented with the rocket icon.
    The green double arrow icon (retweet for Twitter) doesn't look right for the term "boost". Green rocket looks weird, so I use purple.
  • Short usernames (@username) are displayed in timelines, instead of the full account username (@username@instance).
    Despite the guideline mentioned that "Decentralization must be transparent to the user", I don't think we should shove it to the face every single time. There are also some screen-reader-related accessibility concerns with the full username, though this web app is unfortunately not accessible yet.
  • No autoplay for video/GIF/whatever in timeline.
    The timeline is already a huge mess with lots of people, brands, news and media trying to grab your attention. Let's not make it worse. (Current exception now would be animated emojis.)
  • Hash-based URLs.
    This web app is not meant to be a full-fledged replacement to Mastodon's existing front-end. There's no SEO, database, serverless or any long-running servers. I could be wrong one day.

Subtle UI implementations

User name display

User name display

  • On the timeline, the user name is displayed as [NAME] @[username].
  • For the @[username], always exclude the instance domain name.
  • If the [NAME] looks the same as the @[username], then the @[username] is excluded as well.
Boosts Carousel

Boosts Carousel

  • From the fetched posts (e.g. 20 posts per fetch), if number of boosts are more than quarter of total posts or more than 3 consecutive boosts, boosts carousel UI will be triggered.
  • If number of boosts are more than 3 quarters of total posts, boosts carousel UI will be slotted at the end of total posts fetched (per "page").
  • Else, boosts carousel UI will be slotted in between the posts.
Thread number badge (e.g. Thread 1/X)

Thread number badge

  • Check every post for inReplyToId from cache or additional API requests, until the root post is found.
  • If root post is found, badge will show the index number of the post in the thread.
  • Limit up to 3 API requests as the root post may be very old or the thread is super long.
  • If index number couldn't be found, badge will fallback to showing Thread without the number.
Hashtag stuffing collapsing

Hashtag stuffing collapsing

  • First paragraph of post content with more than 3 hashtags will be collapsed to max 3 lines.
  • Subsequent paragraphs after first paragraph with more than 3 hashtags will be collapsed to 1 line.
  • Adjacent paragraphs with more than 1 hashtag after collapsed paragraphs will be collapsed to 1 line.
  • If there are text around or between the hashtags, they will not be collapsed.
  • Collapsed hashtags will be appended with ... at the end.
  • They are also slightly faded out to reduce visual noise.
  • Opening the post view will reveal the hashtags uncollapsed.
Filtered posts
  • "Hide completely"-filtered posts will be hidden, with no UI to reveal it.
  • "Hide with a warning"-filtered posts will be partially hidden, showing the filter name and author name.
    • Content can be partially revealed by hovering over the post, with tooltip showing the post text.
    • Clicking it will open the Post page.
    • Long-pressing or right-clicking it will "peek" the post with a bottom sheet UI.
    • On boosts carousel, they are sorted to the end of the carousel.

Development

Prerequisites: Node.js 22+

  • npm install - Install dependencies
  • npm run dev - Start development server and messages:extract (clean + watch) in parallel
  • npm run build - Build for production
  • npm run preview - Preview the production build
  • npm run fetch-instances - Fetch instances list from joinmastodon.org/servers, save it to src/data/instances.json
  • npm run sourcemap - Run source-map-explorer on the production build
  • npm run messages:extract - Extract messages from source files and update the locale message catalogs
  • npm run git:po-filter - Configure git to use po-filter for diffing .po files

Tech stack

Some of these may change in the future. The front-end world is ever-changing.

Internationalization

All translations are available as gettext .po files in the src/locales folder. The default language is English (en). CLDR Plural Rules are used for pluralization. RTL (right-to-left) languages are also supported with proper text direction, icon rendering and layout.

On page load, default language is detected via these methods, in order (first match is used):

  1. URL parameter lang e.g. /?lang=zh-Hant
  2. localStorage key lang
  3. Browser's navigator.language

Users can change the language in the settings, which sets the localStorage key lang.

Guide for translators

*Inspired by Translate WordPress Handbook:

Technical notes

  • IDs for strings are auto-generated instead of explicitly defined. Some of the benefits are avoiding the "naming things" problem and avoiding duplicates.
    • Explicit IDs might be introduced in the future when requirements and priorities change. The library (Lingui) allows both.
    • Please report issues if certain strings are translated differently based on context, culture or region.
  • There are no strings for push notifications. The language is set on the instance server.
  • Native HTML date pickers, e.g. <input type="month"> will always follow the system's locale and not the user's set locale.
  • "ALT" in ALT badge is not translated. It serves as a a recognizable standard across languages.
  • Custom emoji names are not localized, therefore searches don't work for non-English languages.
  • GIPHY API supports a list of languages for searches.
  • Unicode Right-to-left mark (RLM) (U+200F, &rlm;) may need to be used for mixed RTL/LTR text, especially for <title> element (document.title).
  • On development, there's an additional pseudo-LOCALE locale, used for pseudolocalization. It's for testing and won't show up on production.
  • When building for production, English (en) catalog messages are not bundled separatedly. Other locales are bundled as separate files and loaded on demand. This ensures that en is always available as fallback.

Volunteer translations

Crowdin

Languages chart

Translations are managed on Crowdin. You can help by volunteering translations.

Read the intro documentation to get started.

Self-hosting

This is a pure static web app. You can host it anywhere you want.

Two ways (choose one):

Easy way

❌ NOT recommended.

Go to Releases and download the latest phanpy-dist.zip or phanpy-dist.tar.gz. It's pre-built so don't need to run any install/build commands. Extract it. Serve the folder of extracted files.

Important

Text translations connect to an external service (translang.phanpy.social).

Custom-build way

✅ Recommended.

Requires Node.js.

Download or git clone this repository. Use production branch for stable releases, main for latest. Build it by running npm run build (after npm install). Serve the dist folder.

Important

Text translations connect to an external service (translang.phanpy.social). This can be configured with environment variables if you want to self-host your own instance.

Customization can be done by passing environment variables to the build command. Examples:

PHANPY_CLIENT_NAME="Phanpy Dev" \
    PHANPY_WEBSITE="https://dev.phanpy.social" \
    npm run build
PHANPY_DEFAULT_INSTANCE=hachyderm.io \
    PHANPY_DEFAULT_INSTANCE_REGISTRATION_URL=https://hachyderm.io/auth/sign_up \
    PHANPY_PRIVACY_POLICY_URL=https://hachyderm.io/privacy-policy \
    npm run build

It's also possible to set them in the .env file.

Available variables:

  • PHANPY_CLIENT_NAME (optional, default: Phanpy) affects:
    • Web page title, shown in the browser window or tab title
    • App title, when installed as PWA, shown in the Home screen, macOS dock, Windows taskbar, etc
    • OpenGraph card title, when shared on social networks
    • Client name, when registering the app for authentication and shown as client used on posts in some apps/clients
  • PHANPY_WEBSITE (optional but recommended, no defaults) affects:
    • Canonical URL of the website
    • OpenGraph card URL, when shared on social networks
    • Root path for the OpenGraph card image
    • Client URL, when registering the app for authentication and shown as client used on posts in some apps/clients
  • PHANPY_DEFAULT_INSTANCE (optional, no defaults):
    • e.g. 'mastodon.social', without https://
    • Default instance for log-in
    • When logging in, the user will be redirected instantly to the instance's authentication page instead of having to manually type the instance URL and submit
  • PHANPY_DEFAULT_INSTANCE_REGISTRATION_URL (optional, no defaults):
    • URL of the instance registration page
    • E.g. https://mastodon.social/auth/sign_up
  • PHANPY_PRIVACY_POLICY_URL (optional, default to official instance's privacy policy):
    • URL of the privacy policy page
    • May specify the instance's own privacy policy
  • PHANPY_DEFAULT_LANG (optional):
    • Default language is English (en) if not specified.
    • Fallback language after multiple detection methods (lang query parameter, lang key in localStorage and navigator.language)
  • PHANPY_REFERRER_POLICY (optional, default: origin):
    • Referrer policy for the site. See MDN.
    • This is applied with the <meta> tag on the client-side.
    • The policy can also be set with Referrer-Policy header configured on the server-side (not this variable).
    • Note that since Phanpy uses hash-based URLs, the referrer does not include the hash part.
  • PHANPY_LINGVA_INSTANCES (DEPRECATED, optional, space-separated list, default: lingva.phanpy.social [...hard-coded list of fallback instances]):
    • Specify a space-separated list of instances. First will be used as default before falling back to the subsequent instances. If there's only 1 instance, means no fallback.
    • May specify a self-hosted Lingva instance, powered by either lingva-translate or lingva-api
    • List of fallback instances hard-coded in /.env
    • ↗️ List of lingva-translate instances
  • PHANPY_TRANSLANG_INSTANCES (optional, space-separated list, default: translang.phanpy.social):
    • Specify a space-separated list of instances. First will be used as default before falling back to the subsequent instances. If there's only 1 instance, means no fallback.
    • May specify a self-hosted Translating instance, powered by translang-api.
    • List of instances hard-coded in /.env
  • PHANPY_IMG_ALT_API_URL (optional, no defaults):
    • API endpoint for self-hosted instance of img-alt-api.
    • If provided, a setting will appear for users to enable the image description generator in the composer. Disabled by default.
  • PHANPY_GIPHY_API_KEY (optional, no defaults):
    • API key for GIPHY. See API docs.
    • If provided, a setting will appear for users to enable the GIF picker in the composer. Disabled by default.
    • This is not self-hosted.
  • PHANPY_DISALLOW_ROBOTS (optional, default: not set):
    • Set to any value (true, 1, etc) to override the robots.txt file and disallow all web crawlers from indexing the site

Static site hosting

Try online search for "how to self-host static sites" as there are many ways to do it.

Lingva-translate or lingva-api hosting

⚠️ DEPRECATED. See documentation for lingva-translate or lingva-api.

Translang API hosting

See documentation for translang-api.

Keeping up with releases

  1. 👀 Watch this repo for releases.
  2. 🛜 Subscribe to the feed: https://github.com/cheeaun/phanpy/releases.atom
  3. 👉 Follow Phanpy on Mastodon.
  4. ➡️ Query JSON:

Changelog available here: CHANGELOG.md

Community deployments

These are self-hosted by other wonderful folks.

Note: Add yours by creating a pull request.

There are also mobile app wrappers:

Costs

Costs involved in running and developing this web app:

  • Domain name (.social): USD$23.18/year (USD$6.87 1st year)
  • Hosting: Free
  • Development, design, maintenance: "Free" (My precious time)

Mascot

Phanpy is a Ground-type Pokémon.

Maintainers + contributors

Contributors

Translation volunteers

logo
Hugo
(Hugoglyph)

9244 words
eo, es-ES
logo
Lucsdev_
(lucsdev24)

16801 words
oc, pt-PT, pt-BR
logo
CDN

5980 words
zh-CN
logo
valtlai

6828 words
fi
logo
⁂ josé m.
(ghose)

8398 words
gl
logo
Xabi
(xabi_itzultzaile)

5354 words
eu
logo
Volodymyr
(xen4n)

4622 words
uk
logo
SadmL_AI

4370 words
ru
logo
Francesco
(fracl)

8903 words
it
logo
Vaclovas Intas
(Vac31.)

4024 words
lt
logo
databio

3985 words
ca
logo
ZiriSut

3284 words
kab
logo
ButterflyOfFire ⁂
(BoFFire)

3189 words
ar, fr, kab
logo
Sebastiaan Benjamins
(cbasje)

2849 words
nl
logo
rezahosseinzadeh

2756 words
fa
logo
洪 民憙 (Hong Minhee)
(hongminhee)

2837 words
ko
logo
Robert A.Mason
(A.Mason)

2646 words
pl
logo
Archos
(zkreml)

2219 words
cs
logo
tux93

2129 words
de
logo
voitech

2061 words
pl
logo
nicolas
(nclm)

1895 words
fr
logo
Dizro

1870 words
it
logo
Pixelcode
(realpixelcode)

1753 words
de
logo
detected-rage-colett...

1283 words
ja
logo
tkbremnes

1236 words
nb
logo
/dev/urandom
(dev_urandom)

1235 words
eo
logo
isard

1225 words
ca
logo
Freesia
(Freeesia)

1167 words
ja
logo
Onur Çelik
(uzaylul)

1089 words
tr
logo
ElPamplina

837 words
es-ES
logo
misk

796 words
pl
logo
Micah Blachman
(micahblachman)

771 words
he
logo
Maja Abramski-Kronen...
(rwmpelstilzchen)

683 words
eo, he
logo
M33

660 words
fr
logo
Alexander Sorokin
(Brawaru)

658 words
ru
logo
TAKAHASHI Shuuji
(shuuji3)

556 words
ja
logo
Cairo Braga
(cairobraga)

536 words
ca, pt-BR
logo
alternative

530 words
ko
logo
Thiago Ferrer Morini
(tferrermo)

434 words
es-ES
logo
Manuel Viens
(manuelviens)

377 words
fr
logo
vinzv

370 words
de
logo
LukeHong

363 words
zh-TW
logo
Yukai Huang
(Yukaii)

357 words
zh-TW
logo
xqueralt

350 words
ca
logo
Karla Fejfarová
(karlafej)

346 words
cs
logo
Niklas Amann
(NiklasA95)

278 words
de
logo
GabrielSouzaNeves

262 words
pt-BR
logo
Jaroslav Lichtblau
(svetlemodry)

219 words
cs
logo
gflorian

217 words
de
logo
elissarc

202 words
fr
logo
sona
(mojosoeun)

194 words
ko
logo
R
(Mvtbh)

191 words
fa
logo
Nortix08

186 words
de
logo
Nyanchew
(nyanchew)

181 words
ja
logo
Lîm Tsú-thuàn
(dannypsnl)

179 words
zh-TW
logo
PPNplus

173 words
th
logo
Mr. Will
(MrWillCom)

153 words
zh-CN
logo
Marcin Koziński
(marcin.kozinski)

150 words
pl
logo
Lee Dogeon
(moreal)

141 words
ko
logo
Solène Garda-Krebs
(SoleneGK)

139 words
fr
logo
Usia

135 words
uk
logo
mahefa randimbisoa
(mahefa.randimbisoa)

132 words
fr
logo
Radomír Žemlička
(Razem)

130 words
cs
logo
Martin Modrák
(martinmodrak)

130 words
cs
logo
吳何封
(drydenwu)

129 words
zh-TW
logo
codl

124 words
fr
logo
Sky_NiniKo

121 words
fr
logo
buenanoche

101 words
es-ES
Translate in Crowdin 🚀

Backstory

I am one of the earliest users of Twitter. Twitter was launched on 15 July 2006. I joined on December 2006 and my first tweet was posted on 18 December 2006.

I know how early Twitter looks like. It was fun.

Back then, I made a Twitter clone called "Twig" written in Python and Google App Engine. I almost made my own Twitter desktop client written in Appcelerator Titanium. I gave one of my best talks about the Twitter client in a mini-conference. I built this thing called "Twitter Columns", a web app that shows your list of followings, your followings' followings, your followers, your followers' followers and so on. In 2009, I wrote a blog post titled "How I got started with Twitter". I created two themes for DestroyTwitter (a desktop client made with Adobe Air by Jonnie Hallman) and one of them is called "Vimeo". In 2013, I wrote my own tweets backup site with a front-end to view my tweets and a CouchDB backend to store them.

It's been more than 15 years.

And here I am. Building a Mastodon web client.

Alternative web clients

💁‍♂️ Notice to all other social media client developers

Please, please copy the UI ideas and experiments from this app. I think some of them are pretty good and it would be great if more apps have them.

If you're not a developer, please tell your favourite social media client developers about this app and ask them to copy the UI ideas and experiments.

License

MIT.

About

A minimalistic opinionated Mastodon web client

Topics

Resources

Security policy

Stars

1.5k stars

Watchers

8 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages