Give Extopy a visual direction of its own - #14
Merged
Conversation
The app ships six translations and neither platform advertised them. `CFBundleLocalizations` was missing from the Info.plist, so iOS treated the app as English-only: no Language entry in Settings, and a single language on the App Store listing. `knownRegions` in the project file was already right, which is what made this easy to miss. Android had no `locales_config.xml` and no `android:localeConfig`, so the per-app language picker Android 13+ offers never appeared. Following the system language already worked; choosing per-app did not. Also fixes two escaped apostrophes in the French strings. Compose does not unescape `\'`, so "Nom d\'utilisateur" rendered with the backslash visible. The greeting and empty-state strings the timeline now needs are translated in en, fr, de and es; bs, hr and sr fall back to English until someone who speaks them writes the sentences.
The brand red is fixed, so what this direction spends is not the hue but the
room given to it: it tints every neutral in the app, and only saturates on the
current tab and a liked heart.
The theme is now three files, the way nutrimaxing-app has it. `Color.kt`
builds both schemes around that red — no grey in here is neutral, which is
what stops a mostly-monochrome feed from looking like a system dialog.
`Type.kt` gives the author's name, the handle, the body and the timestamp four
distinct styles; they all shared the default body style before, which is why a
post read as one flat block. `Theme.kt` assembles those with a softer shape
scale, and carries the two colours Material has no role for — a liked heart
and a repost are states of a post, not "primary" or "error". `liked` is
deliberately not the brand red: a heart lighting up in exactly the accent
colour is indistinguishable from the accent.
Three stock components are replaced rather than themed:
- `NavigationBar` becomes a pill floating clear of the screen edge, with the
current tab filled rather than carrying a small indicator behind its glyph.
It respects the navigation-bar inset, so it no longer sits under the gesture
bar. The tab for direct messages says "Messages": "Direct Message" is the
title of the screen but does not fit a quarter of the bar, which is what
used to wrap it onto two lines.
- `OutlinedTextField` becomes a filled pill on `BasicTextField`. The stock one
brings a container, a floating label slot, an indicator line and a 56 dp
minimum height, all of which fight a rounded shape.
- The counters become bubbles grouped on the left instead of icons spread
across the whole card, so they read as actions belonging to the post above
them rather than as a detached toolbar.
The tab bar also stops guessing. It used to match the current tab by comparing
`route.lowercase().startsWith(item.name.replace("_", ""))` — a string
comparison between an enum name and a route class name, which renaming a
screen would have broken in silence. It now asks the destination directly with
`hasRoute`, and raises an event instead of navigating itself.
The fourteen hand-maintained icon XMLs are gone, replaced by Phosphor, which
brings outline and filled weights for everything. Only three of the old ones
came in pairs, and one was not referenced at all.
Finally, an empty timeline says so. It used to render a black screen, which is
indistinguishable from a screen that failed to load.
…bs work The tabs were four navigation destinations, so switching between them ran a navigation transition across the whole screen — for a move that goes nowhere. They are now state inside `MainScreen`, the way nutrimaxing-app has it, held in a `rememberSaveable`. Changing tab redraws the content and nothing else. That also answers where the bar belongs: inside `MainScreen`. A post, a profile or the composer are opened *on top of* a tab, so they simply do not have one — no visibility rule to get wrong. Which makes a back affordance mandatory rather than nice to have: those three screens had a `CenterAlignedTopAppBar` with no navigation icon at all, and without the bar there was no way back on Android, which has no edge swipe. `ExtopyTopBar` gives them one. The rest is the direction applied where it had not been yet — flat cards, grouped counters, pill buttons, the type scale: - `UserCard` loses its elevation, and its biography line no longer leaves a gap when there is no biography. - A profile's three figures stack the number over its label in two different styles, instead of one string split by a newline where both halves looked the same. - "Follow" is the filled button and "Following" the outlined one — the filled state used to be on the wrong one, so following someone made the button louder rather than quieter. - `AccountView` printed the display name twice; the second line was meant to be the handle. - Settings groups its rows into surfaces, and drops the Accounts heading when there are no accounts rather than heading an empty list. - The composer gets the same rounded field as search, and a pill send button. Direct messages and notifications say they are coming instead of rendering nothing: an empty screen reads as a failure, not as an unfinished feature.
The last screen still on stock styling: the headline and description now sit on the type scale instead of on titleLarge and the default body, and the button is a pill like every other primary action in the app.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extopy was Material 3 with a red swapped in: stock components, one type style for everything, and
a palette that was iOS system greys on a Material scheme. This gives it a direction of its own.
#D9534Fis fixed by the project and stays exactly as it is. What changed is how much room itgets: it tints every neutral in the app, and only saturates on the current tab and a liked heart.
The theme
Three files, the way nutrimaxing-app has it.
Color.ktbuilds both schemes around the red — no grey in here is neutral, which is what stops amostly-monochrome feed from looking like a system dialog.
Type.ktgives the author's name, thehandle, the body and the timestamp four distinct styles; they all shared the default body style
before, which is why a post read as one flat block of text.
Theme.ktassembles those with asofter shape scale, and carries the two colours Material has no role for — a liked heart and a
repost are states of a post, not "primary" or "error".
likedis deliberately not the brand red. It used to be exactlyprimaryColor, so a likedheart was indistinguishable from the accent.
repostedColorwas#008000, a pure green belongingto no palette; it is now desaturated to sit beside the red.
Components of our own
Three stock components are replaced rather than themed, because theming them was not enough:
NavigationBar→ a pill floating clear of the screen edge, current tab filled rather thancarrying a small indicator behind its glyph. It respects the navigation-bar inset, which the old
one did not — it sat under the gesture bar.
OutlinedTextField→ a filled pill onBasicTextField. The stock field brings a container,a floating label slot, an indicator line and a 56 dp minimum height, all of which fight a
rounded shape. Used by search and by the composer.
card by
SpaceEvenly, which made them read as a detached toolbar rather than as metadata of thetext above them.
The fourteen hand-maintained icon XMLs are gone, replaced by Phosphor, which brings outline and
filled weights for everything. Only three of the old ones came in pairs, and one was not referenced
at all.
Tabs are no longer destinations
The four tabs were navigation destinations, so switching between them ran a navigation transition
across the whole screen — for a move that goes nowhere. They are now state inside
MainScreen,held in a
rememberSaveable.That also settles where the tab bar belongs: inside
MainScreen. A post, a profile or the composerare opened on top of a tab, so they simply do not have one — there is no visibility rule to get
wrong.
Which makes a back affordance mandatory rather than nice to have: those three screens had a
CenterAlignedTopAppBarwith no navigation icon at all, so without the bar there was no way backon Android, which has no edge swipe.
ExtopyTopBargives them one.The bar also stopped guessing which tab is current. It used to compare
route.lowercase().startsWith(item.name.replace("_", ""))— a string comparison between an enumname and a route class name, which renaming a screen would have broken in silence.
Bugs found on the way
Not design, but they were in the screens being touched:
already following state, so following someone made the button louder rather than quieter.
AccountViewprinted the display name twice — the second line was meant to be the handle.rendering fault.
empty screen reads as a failure, not as an unfinished feature. So does an empty timeline, which
used to be a black screen.
Localisation
Following
LOCALIZATION.mdin nutrimaxing-docs turned up two real gaps:CFBundleLocalizationswas missing from the Info.plist, so iOS treated the app asEnglish-only: no Language entry in Settings, and one language on the App Store listing, for six
translations actually shipped.
knownRegionswas already right, which is what made it easy tomiss.
locales_config.xmland noandroid:localeConfig, so the per-app language picker Android13+ offers never appeared.
Also two escaped apostrophes in the French strings: Compose does not unescape
\', so"Nom d'utilisateur" rendered with the backslash visible.
New strings are translated in en, fr, de and es; bs, hr and sr fall back to English until someone
who speaks them writes the sentences. The three profile figures had their labels derived from the
existing translations rather than retranslated.
Verified
Android debug and release, the 17 tests, both iOS frameworks, and the app running on the simulator
against a local backend — in both light and dark, which is where the two grounds were tuned.
Not verified: navigation by hand. Tapping the simulator needs an accessibility permission the
terminal does not have, so switching tabs, opening a post and coming back are checked by reading
the code, not by using it. Worth thirty seconds of your thumb before merging. Android runtime is
still unverified too — the local AVDs have no system image installed.
Still open, and untouched here
TimelineViewModel._usersis never populated, so theUserCardthis PR restyles never actuallyappears in the timeline. And
LogoutUseCasestill has no entry point in Settings. Both predatethis branch.
Note that merging runs the deploy jobs again; they currently fail on their own (missing Ruby on the
runner, and the fastlane match SSH key), so nothing ships either way.