window.innerWidth can briefly report the desktop viewport width when
the page is restored from a long Android background session, causing
openChat() to skip adding chat-open even though chatIsOpen is true.
Replace all window.innerWidth checks with mobileQuery.matches so the
JS breakpoint always agrees with CSS. Also swap the resize listener
for mobileQuery.addEventListener('change') which only fires when the
breakpoint actually crosses, and add a visibilitychange handler to
re-apply chat-open when the app returns to the foreground.
Android 10+ enforces W^X (Write XOR Execute) — UPX's self-extracting
stub allocates RWX memory at runtime, which the kernel kills immediately.
Skip UPX for the armeabi-v7a matrix entry so the binary loads cleanly.
- Persist display names in per-channel JSON cache files (Name +
DisplayName fields on cachedChannel). GetAllTitles reads all ch_*.json
files; PutTitle updates a channel file in-place without losing messages.
- Replace blocking FetchTitles calls in fetchMeta and refreshChannel with
an instant disk read (GetAllTitles) applied before the SSE broadcast,
so channels appear with cached titles immediately on every load.
- ensureTitlesFetched runs in a single background goroutine (titlesMu +
titlesLoading guard prevents duplicates). On success it persists titles
and pushes an SSE update. On error or empty response it backs off for
5 minutes so an old server does not cause endless retries.
- Block 0 of TitlesChannel now carries a uint16 total-block-count prefix
(added in rebuildTitlesBlocks). FetchTitles reads the count from block 0
and fetches all remaining blocks in parallel instead of sequentially.
- FetchTitles timeout raised from 10 s to 1 minute.
- Channel list: always show @handle as subtitle below display name for Telegram channels
- Chat header: show @handle as subtitle below channel title
- Mobile (<768px): hide Search/Export buttons; show them in a ⋮ kebab menu (rightmost button)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add dedicated TitlesChannel (0xFFF9) following the VersionChannel pattern
- Server encodes name→title map via EncodeTitlesData/DecodeTitlesData
- Metadata wire format unchanged for backward compatibility with old clients
- All three fetchers (public Telegram, MTProto, X/Nitter) extract and store display names
- Client fetches TitlesChannel with a 10s deadline; falls back to channel handles gracefully on old servers
- Old clients are unaffected — they never query 0xFFF9
- Add DisplayName field to ChannelInfo in the wire protocol
- Add displayNames map to Feed; SetChannelDisplayName no longer mutates
f.channels, keeping handles stable for cache keys and management
- Public fetcher: extract title via extractChannelTitle, pass to feed
- Telegram fetcher: capture ch.Title / user.FirstName from MTProto API
- Frontend: render DisplayName as sidebar label, keep Name as identifier
fetchChannel now returns the channel's display title extracted from the
tgme_channel_info_header_title element. SetChannelDisplayName replaces
the raw @handle in the feed metadata with the human-readable title after
the first successful fetch.
Previously linkify received pre-escaped text, so & in URLs became &
causing the regex to truncate URLs at query-string boundaries. Now
linkify escapes HTML internally so URLs are matched against raw text.
Also adds [label](url) markdown link support.