Commit Graph

2256 Commits

Author SHA1 Message Date
Jorge Martin Espinosa f2883ef9df Fix flaky test in DefaultVoiceMessageComposerPresenterTest (#7487) 2026-08-20 11:49:59 +02:00
hayyaksi 2525019ca1 Show the right image when opening one that has not finished sending (#7359)
* Show the right image when opening one that has not finished sending

* Reuse the timeline media gallery for events that have no event id
2026-08-20 11:16:13 +02:00
hayyaksi 7341feeed8 Model the fake voice recorder on a cancellable recording job (#7466)
* Model the fake voice recorder on a cancellable recording job

FakeVoiceRecorder pushed a whole recording synchronously from a single
suspending call, which made it both weaker and stricter than
DefaultVoiceRecorder.

Every level emission carried the same elapsed time and the emissions were
coalesced by Molecule, so no intermediate recording state was observable and
the level-limit test had to skip half the emissions to compensate. Each level
now advances the test time source and suspends for its share of the recording
duration, so the states arrive one at a time and that magic number is gone.

Call verification moved to the constructor lambdas the rest of the repo uses,
in place of hand-rolled counters and assertCalls. The counters discarded the
argument, so nothing pinned that cancelling a recording stops it with
cancelled = true; that is now asserted. Truth is no longer needed by the
fixture module.

A second startRecord() no longer raises an error where production only warns
and returns, and stopping or deleting terminates an in-flight emission loop
the way cancelling the real recording job does, so a stale recording state can
no longer arrive after the recording has finished.

* Keep each recording's level loop to its own recording

The level loop checked a single isRecording flag, which cannot tell "this
recording was cancelled" from "a later recording has started". After a cancel
followed by a new start, the first loop resumed, saw the flag set again, and
advanced the shared test time source, so the new recording reported the elapsed
time of both. Each recording now carries an id and its loop stops as soon as
that id is no longer the active one.
2026-08-19 13:44:11 +02:00
hayyaksi ee1348e19e Give the room name in the timeline top bar a 48dp tap target (#7450)
* Give the room name in the timeline top bar a 48dp tap target

The row carrying the room avatar and name is the control that opens the
room details, but it is only as tall as its content, which is under the
48dp minimum recommended for a touch target. Constrain it to a minimum
height of 48dp. The children stay centred inside the app bar, so nothing
moves visually.

* Update screenshots

The accessibility snapshot of the timeline is the only one the change
affects: the node which opens the room details now reports a 48dp tall
bounding box. The room name, the avatar and the icons stay where they
were.
2026-08-19 12:29:24 +02:00
hayyaksi cb6ac751cc Refresh the room members when a mention is started (#7438)
* Refresh the room members when a mention is started

The mention suggestions read room.membersStateFlow, which is a cache filled
only by an explicit updateMembers() call. Nothing calls it while a room is
open, so a member who joined after the room was opened never appears in the
suggestion list until the room is left and entered again.

Call updateMembers() when a mention search starts. The trigger is
distinctUntilChanged on "the current suggestion is a mention", so it fires
once per mention rather than on every keystroke, and fetchRoomMembers already
no-ops while a fetch is in flight.
2026-08-17 21:10:08 +02:00
Benoit Marty a740423343 Sync strings. 2026-08-17 11:34:52 +02:00
Benoit Marty c32f81f696 Revert "Keep manually revealed media visible when re-entering a room" 2026-08-14 10:49:24 +02:00
Jorge Martin Espinosa 22195a47f2 Move the custom tile server URL config to the Element wellknown content (#7416)
* Move the custom tile server URL config to the Element wellknown content

* Iteration: use whole `MapTilerConfig` in the remote config
2026-08-14 08:20:37 +02:00
Benoit Marty 8bf5b9dd6f Merge pull request #7349 from hayaksi1/fix/4757-timeline-grouping-timeout
Do not group timeline messages sent more than five minutes apart
2026-08-13 09:23:01 +02:00
Benoit Marty 1abe26f4d8 Merge pull request #7354 from hayaksi1/fix/5319-linkify-matrix-uri
Turn a matrix: URI in a message into a pill
2026-08-13 09:17:32 +02:00
Benoit Marty 00e9a8ab71 Fix wrong TextComposer placeholder displayed when in a thread.
We cannot only use MessageComposerMode since use can edit a message in a thread, so this is becoming handly.
2026-08-12 17:05:08 +02:00
Benoit Marty 539437da22 Merge pull request #7343 from hayaksi1/fix/7190-forward-live-location
Hide the forward action on live location shares
2026-08-12 13:47:36 +02:00
Benoit Marty 6cc581d2d3 Merge pull request #7379 from hayaksi1/fix/7314-invalid-pinned-events
Do not display pinned events that the client cannot render
2026-08-12 13:44:48 +02:00
hayaksi1 055fea8d25 Hide the forward action on live location shares
The timeline action sheet offered Forward on a live location share, but
forwarding one always failed: Mode.Live maps to the SDK's
MsgLikeKind.LiveLocation, which RoomContentForwarder rejects outright. So
the action was shown for exactly the set of events on which it could
never work.

canBeForwarded() now returns true for a location event only when its mode
is Static. Ended live shares keep Mode.Live and fail for the same reason,
so they are excluded too. ActionListPresenter needs no change: its check
is the function's only call site in the repo, so the single gate covers
the timeline sheet, the threaded timeline and the pinned messages list.

Hiding the action mirrors Element Web and the sticker exclusion already
present in the same function. Converting a live share into a static
m.location on forward would be a product decision and is out of scope.

Relates to https://github.com/element-hq/element-x-android/issues/7190
2026-08-11 17:48:45 +03:00
ganfra 6d6ca97899 Merge pull request #7328 from element-hq/feature/fga/user_status_last_pieces
User status last pieces
2026-08-11 11:11:12 +02:00
ganfra 0de78ed956 Merge branch 'develop' into feature/fga/user_status_last_pieces 2026-08-11 10:04:18 +02:00
bmarty 17c5d7db59 Sync Strings from Localazy 2026-08-11 09:27:29 +02:00
Benoit Marty a95f02497e Merge pull request #7378 from hayaksi1/fix/6853-remember-revealed-images
Keep manually revealed media visible when re-entering a room
2026-08-10 19:23:39 +02:00
hayyaksi 348c7bcea8 Expand the emoji picker to full height when the keyboard is shown (#7382)
* Expand the emoji picker to full height when the keyboard is shown

The picker content is rendered with fillMaxSize, so the sheet measures itself to
the full window height. That makes the partially expanded anchor available, and
the modal sheet auto-shows at that anchor, half way down the screen.

The sheet reserves the IME inset through its content window insets, but applies
that padding at its own bottom edge, which is off-screen while it rests half way
down. The band that is both on-screen and above the keyboard is then only about
a tenth of the screen, which is why the matching emoji end up behind it.

Expand the sheet while the keyboard is visible so its bottom edge returns to the
screen edge and the inset it already reserves lands on top of the keyboard.
Modifier.imePadding on the sheet content is not an option: the sheet applies the
insets with windowInsetsPadding, which consumes them, so a nested imePadding
reads zero.

* Cover the keyboard driven sheet expansion with a test

The IME reaction moves into ExpandSheetWhenImeIsVisible so a Robolectric test can
drive it: dispatching window insets to the composition flips
WindowInsets.isImeVisible, and the sheet state is observable from the test. Both
the shown and the hidden case are asserted, which is the patch coverage this
change was missing.

* Do not leave a blank strip below the expanded emoji search

The 10dp of bottom padding on the search bar separates the collapsed
field from the category tabs underneath it. It stayed applied once the
search bar expanded to fill the picker, so the picker stopped 10dp short
of its own bottom edge.

That strip used to sit below the visible part of a half height sheet.
Now that the sheet expands against the keyboard it reads as a gap
between the results and the keyboard, so only pad while collapsed.
2026-08-10 16:35:38 +02:00
Jorge Martin Espinosa e101963814 Display 'add reactions' menu in the threads screen (#7388) 2026-08-10 15:38:57 +02:00
hayaksi1 1b833df727 Expose the revealed event ids as a SnapshotStateSet
Restore the snapshot backed set the store started with and declare
allowedEventIds as SnapshotStateSet so the interface says out loud that
reads of it are observable.

TimelineProtectionPresenter reads the set inside derivedStateOf, so a
plain set silently stops the timeline from re-rendering when an event is
revealed. Naming the type keeps that requirement from being mistaken for
an implementation detail.
2026-08-10 16:36:36 +03:00
Benoit Marty 1693d325c1 Update UI according to https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=5781-5862 2026-08-10 12:12:55 +02:00
hayaksi1 09efb04a8f Hold the revealed event ids in a plain set
The store no longer needs a mutableStateSetOf: an ordinary Set kept in a
mutableStateOf does the same job, and matches DefaultMediaPreviewConfigStateStore.

The Compose state itself has to stay. TimelineProtectionPresenter derives
ProtectionState from the store inside a derivedStateOf, so the set must be a
snapshot object for a reveal to invalidate it and reach the timeline. Backing it
with a plain mutableSetOf leaves the presenter with nothing to observe and it
stops emitting after ShowContent.
2026-08-10 10:59:22 +03:00
hayaksi1 f0e791001f Drop the comments from the matrix URI pillification
Review feedback: the regex name and the test names carry the meaning.
2026-08-09 15:39:18 +03:00
hayaksi1 b725d37b0e Do not display pinned events that the client cannot render
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.
2026-08-09 15:20:13 +03:00
hayaksi1 6533a10ab9 Keep manually revealed media visible when re-entering a room
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.
2026-08-09 15:19:46 +03:00
hayaksi1 38398c24b2 Show a Message not found placeholder when a replied to message cannot be loaded
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.
2026-08-09 15:19:45 +03:00
hayaksi1 72cc8f848d Drop the comments from the grouping timeout
Review feedback: the constant name and the test names carry the rule.
2026-08-09 14:51:30 +03:00
hayaksi1 4b85e2d31e Turn a matrix: URI in a message into a pill
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
2026-08-09 04:09:32 +03:00
hayaksi1 b5e95e27d4 Do not group timeline messages sent more than five minutes apart
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
2026-08-09 03:51:48 +03:00
ganfra 14c0944d22 Merge branch 'develop' into feature/fga/user_status_last_pieces 2026-08-07 11:35:19 +02:00
Benoit Marty d1cf1aa09f Fix deprecation warning on androidx.compose.material3.ListItem 2026-08-06 18:35:17 +02:00
ganfra 494dc026fa Create fun ProfileDetails.toMatrixUser method 2026-08-05 16:14:59 +02:00
ganfra 3933d5ab8c Merge pull request #7277 from element-hq/sync-localazy
Sync Strings
2026-07-28 16:44:21 +02:00
ganfra ea3788abd1 Sync Strings from Localazy 2026-07-28 14:51:37 +02:00
Jorge Martin Espinosa 0cd163a680 Import Compound tokens from v10.2.3 (#7272)
* Import Compound tokens from `v10.2.3`

* Remove manually added `bgSubtleTertiary` semantic color: it's in Compound now

* Bump enterprise module to a compatible commit
2026-07-28 09:07:24 +00:00
Jorge Martin Espinosa adc1b50492 Fix insets in several screens (#7285)
* Fix insets in several screens

UI components in the room list, room member list, etc. now have better insets based on `WindowInsets.safeDrawing`

* Update screenshots

---------

Co-authored-by: ElementBot <android@element.io>
2026-07-28 10:29:42 +02:00
Jorge Martin Espinosa 77edfec61a Use the right insets in the home screen UI (#7271)
* Use the right insets in the home screen UI

That's `WindowInsets.statusBar` for the `Scaffold` component, then `WindowInsets.navigationBar` for the `LazyColum.contentPadding`

* Apply it to other screens with long lists of items too

* Apply the insets to `PreferencePage` too

* Apply inset changes to other screens with scrollable content

* Add helper insets/padding values based on review suggestions.

* Use the newly added insets/padding values in several scaffold and scrollable columns

---------

Co-authored-by: ElementBot <android@element.io>
2026-07-27 18:17:14 +02:00
Jorge Martin Espinosa 0442fbd307 Fix media viewer for medias not related to events displaying invalid state (#7280)
* When no URL is provided to `ContentValidationState.collectMediaState`, assume it's valid

That way we can do:

`if (validationState.isValid() && state.collectMediaState(possiblyNullUrl).isValid()) ...`

* Remove the `eventId` param from `ContentScannerService.scan`

It's not really used, since we then provide a state that can be mutated, and fetching that is what the previous parameter was used for

* Make sure to always validate the medias in the viewer, both the actual media and the thumbnail

* Update screenshots

---------

Co-authored-by: ElementBot <android@element.io>
2026-07-27 18:16:35 +02:00
bmarty 892b384f07 Sync Strings from Localazy 2026-07-27 01:07:51 +00:00
ganfra 29d9e7c1ba Merge branch 'develop' into feature/valere/timeline_active_call_rendering
# Conflicts:
#	tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Night_0_en.png
2026-07-23 16:24:11 +02:00
ganfra 907579d59f Merge pull request #7232 from element-hq/feature/fga/display_user_status
WIP : display user status
2026-07-23 15:10:19 +02:00
ganfra 93786cdcbb Use roomMember.getBestName() in SuggestionPickerView 2026-07-23 14:23:05 +02:00
Valere f22e4b3314 Merge branch 'develop' into feature/valere/timeline_active_call_rendering 2026-07-23 13:40:38 +02:00
Valere 19aabd4211 review cleanup 2026-07-23 13:40:08 +02:00
ganfra b9831734a0 Take DisplayedStatus directly in DisplayNameWithStatus and always show emoji 2026-07-22 17:28:30 +02:00
ganfra 3fdd6be832 Extract dmUserStatus helper for DM user status resolution 2026-07-22 17:28:30 +02:00
ganfra 2a86c69b9d Sync user status strings with Localazy and tidy formatting
# Conflicts:
#	libraries/ui-strings/src/main/res/values/temporary.xml
2026-07-22 17:28:29 +02:00
ganfra 06a903718b Display user status next to display names across rooms and profiles 2026-07-22 17:27:21 +02:00
ganfra 19c38f7e7f Fix lint, detekt and konsist checks 2026-07-22 16:46:58 +02:00