Phanpy ===
Minimalistic opinionated Mastodon web client.
🗣️ Pronunciation: /fænpi/ (FAN-pee) 🔊 Listen
This is an alternative web client for Mastodon.
- !GitHub Release -production branch
- break less often
- slower fixes unless critical
- !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
- 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
- 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)
- Check every post for
inReplyToIdfrom 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
Threadwithout the number.
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.
Development
Prerequisites: Node.js 22+
npm install- Install dependenciesnpm run dev- Start development server andmessages:extract(clean+watch) in parallelnpm run build- Build for productionnpm run preview- Preview the production buildnpm run fetch-instances- Fetch instances list from joinmastodon.org/servers, save it tosrc/data/instances.jsonnpm run sourcemap- Runsource-map-exploreron the production buildnpm run messages:extract- Extract messages from source files and update the locale message catalogsnpm run git:po-filter- Configuregitto usepo-filterfor diffing.pofiles
Tech stack
- Vite - Build tool
- Preact - UI library
- Valtio - State management
- React Router - Routing
- masto.js - Mastodon API client
- Iconify - Icon library
- Lingui - Internationalization
- Vanilla CSS - _Yes, I'm old school._
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):
- URL parameter
lange.g./?lang=zh-Hant localStoragekeylang- Browser's
navigator.language
localStorage key lang.
Guide for translators
*Inspired by Translate WordPress Handbook:
- Don’t translate literally, translate organically.
- Try to keep the same level of formality (or informality)
- Don’t use slang or audience-specific terms
- Be attentive to placeholders for variables. Many strings have placesholders e.g.
{account}(variable),<0>{name}0>(tag with variable) and#(number placeholder). - Ellipsis (…) is intentional. Don't remove it.
- Date timestamps, date ranges, numbers, language names and text segmentation are handled by the ECMAScript Internationalization API.
Intl.DateTimeFormat - e.g. "8 Aug", "08/08/2024"
- Intl.RelativeTimeFormat - e.g. "2 days ago", "in 2 days"
- Intl.NumberFormat - e.g. "1,000", "10K"
- Intl.DisplayNames - e.g. "English" (en) in Traditional Chinese (zh-Hant) is "英文"
- Intl.ListFormat - e.g. "A, B, and C"
- Intl.Locale (with polyfill for older browsers)
- Intl.Segmenter (with polyfill for older browsers)
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.
- There are no strings for push notifications. The language is set on the instance server.
- Native HTML date pickers, e.g.
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,) may need to be used for mixed RTL/LTR text, especially forelement (document.title). - On development, there's an additional
pseudo-LOCALElocale, 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 thatenis always available as fallback.
Volunteer translations
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:
PHANPY_WEBSITE(optional but recommended, no defaults) affects:
PHANPY_DEFAULT_INSTANCE(optional, no defaults):
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):
https://mastodon.social/auth/sign_up
PHANPY_PRIVACY_POLICY_URL(optional, default to official instance's privacy policy):
PHANPY_DEFAULT_LANG(optional):
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):
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]):
/.env
- ↗️ List of lingva-translate instances
PHANPY_TRANSLANG_INSTANCES(optional, space-separated list, default:translang.phanpy.social):
/.env
PHANPY_IMG_ALT_API_URL(optional, no defaults):
PHANPY_GIPHY_API_KEY(optional, no defaults):
PHANPY_DISALLOW_ROBOTS(optional, default: not set):
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
- 👀 Watch this repo for releases.
- 🛜 Subscribe to the feed: https://github.com/cheeaun/phanpy/releases.atom
- 👉 Follow Phanpy on Mastodon.
- ➡️ Query JSON:
Changelog available here: CHANGELOG.md
Community deployments
These are self-hosted by other wonderful folks.
- halo.mookiesplace.com by @mookie@suigow.xyz
- phanpy.app by @bumble@ibe.social
- phanpy.bauxite.tech by @b4ux1t3@hachyderm.io
- phanpy.blaede.family by @cassidy@blaede.family
- phanpy.crmbl.uk by @snail@crmbl.uk
- phanpy.cz by @zdendys@mamutovo.cz
- phanpy.fulda.social by @Ganneff@fulda.social
- phanpy.gotosocial.social by @admin@gotosocial.social
- phanpy.hear-me.social by @admin@hear-me.social
- phanpy.mastodon.world by @ruud@mastodon.world
- phanpy.mstdn.mx by @maop@mstdn.mx
- phanpy.social.tchncs.de by @milan@social.tchncs.de
- phanpy.tilde.zone by @ben@tilde.zone
- phanpy.vmst.io by @vmstan@vmst.io
- phanpy.linuxusers.in by @dharmik@linuxusers.in
- phanpy.blablalinux.be by @blablalinux@mastodon.blablalinux.be
- fedi.woodpeckersnest.space by @roughnecks@mastodon.social
Note: Add yours by creating a pull request.
There are also mobile app wrappers:
- iPhanpy by @fantinel@hachyderm.io (iOS) — App Store
- Phandroid by @thoralf@gruene.social (Android) — Installation
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.
Design
All design files are in the design folder.
Logo
- No background:
logo-4.svg,logo-4.png - With background:
logo-bg-4.svg,logo-bg-4.png
Web badges
phanpy-88x31: GIF (560 B), PNG (436 B), WebP (414 B)phanpy-80x15: GIF (263 B), PNG (213 B), WebP (172 B)
Source files
- Affinity Designer:
logo.af,logo.afdesign - Aseprite:
phanpy.aseprite,phanpy-80x15.aseprite,phanpy-colors.aseprite
Maintainers + contributors
Translation volunteers