The pinned events timeline is built with an exclusion filter rather than an
allowlist, so an event of a type the app has no renderer for reaches both
pinned surfaces. The banner falls through to the formatter catch-all and reads
"Unsupported event", and the list renders an unknown content tile.
Drop unknown content and message-like or state events that failed to parse from
both surfaces, along with any day divider left without an event to introduce,
and show the empty state when nothing renderable remains. The spec makes this
the client's call. Such an event can still be unpinned from the room timeline,
where it is still rendered.
Undecryptable and redacted events are untouched: they carry information the
user needs.
The set of revealed event ids lived on TimelineProtectionPresenter, which is
scoped to the room graph. That graph is rebuilt every time the room is opened,
so the set was empty on re-entry and every revealed image was hidden again.
Move it into a session scoped TimelineProtectionStore. The store is in memory
rather than persisted: revealing media is a privacy control and there is no
hide action today, so keeping the reveals for the session leaves restarting the
app as a way to undo one, and the set is dropped on logout.
The account provider search treated its whole input as a host, so a full user
ID was probed as https://@alice:example.org, which never resolves, and the
permissive URL fallback then offered that invalid address as a provider.
Recognise a complete user ID and resolve the homeserver from its server name,
preserving a port when there is one. Input that starts with @ but is not yet a
complete user ID resolves nothing, so no candidate is produced while the user
is still typing. Detection reuses MatrixPatterns.isUserId and UserId.domainName
rather than a new pattern.
When the replied to event could not be fetched, the reply preview rendered the
raw SDK error string in critical styling, and tapping it emitted a focus
request that could never succeed, so the user got a dead end dialog.
Render the localised error_message_not_found string in the neutral italic style
already used for redacted and undecryptable replies, and drop the click handler
for that case. The existing key is reused rather than adding a new one; a "you
do not have access" wording was rejected because this state covers any failure
to load the event, not only a permission problem.
The blocked users list displayed each Matrix ID but offered no way to get it
out of the app, which is what someone who blocked a user for abuse needs in
order to report them onwards.
Long press now copies the Matrix ID and confirms with a snackbar, reusing the
ClipboardHelper and SnackbarDispatcher idiom from the user profile screen. A
short tap still opens the unblock confirmation. The row declares a long click
label so the action is announced by a screen reader. No new string is needed.
After leaving a room listed in a space, the room stayed in the child list with
a Join button, even though it can only be entered with a new invitation. It
disappeared only after leaving and reopening the space, because the server
omits it from the refetched hierarchy.
Apply the same rule locally in the presenter. The filter keys on the LEFT
membership combined with an invite join rule rather than on the derived
visibility, which collapses knock and unknown join rules into private and
would wrongly remove the Join button from knockable rooms.
Two of the three modes on the room notification settings screen had
screen-scoped keys; the third reused common_mute, which also labels the Mute
action button on the room details screen. A translator could not fix one
occurrence without breaking the other.
Add screen_room_notification_settings_mode_mute and use it at both places on
that screen. The English value is unchanged, so nothing renders differently.
The action button keeps common_mute, which is the one context where the
generic verb is correct.
A permalink written as a matrix: URI stayed plain text, while the same
link in matrix.to form became a pill. The pillifier only scanned
Patterns.WEB_URL, which cannot match a URI with no web scheme, even though
the permalink parser already understands matrix: URIs.
Those URIs are now scanned for as well, before the web pass, since
Patterns.WEB_URL otherwise claims the bare domain inside the URI and
whichever pass runs first wins the range. Each match also gets a URLSpan:
the timeline only reports URLSpans as clicks, so without one the pill
would look right and do nothing.
The composer path is untouched, since it pillifies without permalinks.
Relates to https://github.com/element-hq/element-x-android/issues/5319
A DM whose other member has left still offered the call button. Starting
that call put the user alone in a room nobody could join, with no way out
of the call screen.
The presenter now clears canStartCall for a DM with no other active
member. activeMembersCount counts joined and invited members, so a fresh
DM whose peer has been invited but has not joined yet can still start a
call. Joining an already ongoing call is untouched, so a call started
before the other member left stays reachable.
Relates to https://github.com/element-hq/element-x-android/issues/6189
Consecutive messages from the same sender were grouped into one bubble
block no matter how far apart they were sent, so a reply hours later
looked like part of the earlier burst and lost its own timestamp header.
computeGroupPosition now folds a five minute limit into the two
groupability flags it already computes. The delta is symmetric between a
pair of neighbours, so both items always agree on where the block splits
and every branch of the existing decision table keeps working unchanged.
Breaking a group on reactions, which the iOS client also does, is a
separate divergence and is not part of this.
Relates to https://github.com/element-hq/element-x-android/issues/4757
MediaStore de-duplicates the display name of a new file itself, but its
uniquifier gives up after 32 collisions and throws IllegalStateException.
Saving then failed with "Sorry, an error occurred" and wrote nothing, and
because image.png is the name most screenshots arrive with, a user who
saves media regularly could reach a state where that name never saved
again without deleting files from Downloads by hand.
A new saveWithUniqueFileName helper in androidutils retries the insert
once with a suffixed name when the platform reports it cannot build a
unique one; a second failure propagates so genuine errors still surface.
withFileNameSuffix builds that name by inserting the suffix before the
extension, and handles names without one.
Both call sites are fixed: AndroidLocalMediaActions and DefaultFileSave
carried byte-identical logic and therefore the identical defect. While in
AndroidLocalMediaActions, a null output uri from ContentResolver.insert
now throws instead of reporting success without writing anything.
The pre-Android-Q path is untouched: it overwrites rather than
uniquifying, so it never reaches this limit.
Relates to https://github.com/element-hq/element-x-android/issues/6371
Some room list filters are mutually exclusive, and the UI expresses that
by hiding the incompatible chips rather than disabling them. Nothing
enforced the rule on the write path, so two chips tapped in the same
frame, before the list could recompose, both applied. The room list then
matched nothing while the offending chip was hidden, leaving no way to
see or undo what was filtering the rooms.
DefaultFilterSelectionStrategy.select() now ignores a filter that is
incompatible with one already selected, so the same table buildFilters()
uses to hide chips also governs the selection itself. The currently
applied filters win and the late tap is dropped, which matches the
existing design where an incompatible chip is already on its way out of
the UI when it is tapped.
toggle(), deselect(), clear() and the FilterSelectionStrategy interface
are unchanged.
Relates to https://github.com/element-hq/element-x-android/issues/5383
When a homeserver publishes a map_style_url in its .well-known file, the
app appended its own MapTiler API key to it, producing a URL with two
query strings that the tile server rejects. The map then failed to load
for every user of that homeserver, with no way for the operator to opt
out.
MapTilerTileServerStyleUriBuilder.build() now appends ?key= only to the
URL it composes itself from the bundled base url and map ids. A non
blank customMapStyleUrl is returned verbatim, so the operator's own
credentials survive. This matches Element Web and the self-hosted tile
server documentation, both of which treat map_style_url as complete.
The two existing tests that pinned the old behaviour are updated, and
cases are added for a custom URL that already carries a key, one that
carries an unrelated query string, and a blank url falling back to the
built-in style.
Relates to https://github.com/element-hq/element-x-android/issues/7289