video().track() in the buffer format callback takes the libvlc input lock,
which stop() holds while it waits for the decoder thread running that
callback. Take the video track from the media info only.
* android, desktop: open group member profile without waiting for the core
Tapping a member avatar awaited apiGroupMemberInfo and apiGetGroupMemberCode
before the profile modal was created. Both are single-row queries, 1-2 ms when
the core is idle, but sendCmd is serialized against everything else the core is
doing, so while it is busy - startup, a batch of incoming events, a long database
operation - the tap produces nothing at all until the core drains.
The modal is now opened first and the two values arrive in state the tap handler
creates, so the profile opens at the speed of the UI. The card is shown from the
member that is already known at the tap, preferring the model's copy once it is
there, so the member no longer has to be written to the model before the modal
can be shown.
* plans: open member profile without waiting for the core
* android, desktop: keep member profile rows in place while they load
The security code and the connection stats arrive after the profile is shown, so
"Verify security code" and the Servers section appeared under the already
visible card and pushed the rows below them down.
They are now rendered from the first frame in their final positions, disabled,
and enabled in place when the data arrives - SectionItemView already drops the
clickable modifier when disabled, so nothing moves. Only what can be decided from
the member known at the tap is reserved: "Fix connection" is not, as it applies
only while a ratchet needs syncing and a placeholder for it would disappear on
nearly every open.
* android, desktop: reserve the network status row with the servers
The reserved Servers section had three rows where the loaded one has four, so
everything below it - "Block member" - moved down by one row when the stats
arrived.
Network status looks like agent state that cannot be predicted before loading,
but connSubStatus returns Just whenever the connection has receiving queues, and
that is the same list that decides whether "Receiving via" is rendered. The two
rows appear together, so the status row is now reserved with them.
---------
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
* ui: blur media by resampling the preview, not a per-frame effect
Modifier.blur put a BlurEffect on a display-sized graphics layer, so the
Gaussian was re-evaluated on every frame the media was drawn. It also
requires RenderEffect, which Android applies only from API 31, so below
Android 12 media was drawn unblurred while the setting read as on.
A blur and a downscale discard the same thing - detail finer than their
radius - so the preview is now resampled to about one pixel per radius and
stretched back. That runs once when the item composes, needs no
RenderEffect, and removes the detail irreversibly rather than convolving
it. The first resampling step bounds both sides, so no image, however
shaped, can produce a large intermediate.
While the blur hides the media the file is also left unread, so an image
scrolled past is no longer read, decoded at its full size and kept in the
image cache only to be hidden again. blurHidesMedia() is the single
definition that both the drawing and the loading decision use, so they
cannot disagree about whether the media is on screen.
* plans: blur media by resampling the preview
* ui: shorten comments in media blur
* ui: smooth the media blur and match its strength to the old one
Stretching the resampled preview straight to the screen showed its pixel grid. Double it back
up until its longest side is at least half the reference width before drawing, so the last
stretch is short and the tents compose into a bell. The reference width moves from 360 to 400:
fitted against Gaussians, 360 blurred 5-75% more than Modifier.blur did, 400 is within 10% at
every setting.
Skia's raster blur runs at full resolution, so each bar blurred width x (bar + 6 sigma) pixels every frame: about
80 ms per frame for the two bars of a chat at the default radius, which cut scrolling to 5-9 fps on the software
renderer. Scaling the layer cannot help, because a layer's filter is evaluated in device space and the scale is
applied to the sigma too. The bar is now drawn into an offscreen surface up to 8x narrower and blurred there, which
measured 5-6x cheaper and within 0.25/255 of the original.
Blurring a copy means the bar no longer follows the content on its own, so the scroll containers bump a version on
the app bar handler and the bar depends on it.
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
In channels members are usually connected via relays, so they are memberCurrent
but not memberActive, and #7255 widened the condition for requesting the member
security code to cover them. It updated GroupChatInfoView and
MemberSupportChatView, but not the same code in ChatView, so "Verify security
code" is not offered when the profile is opened by tapping member avatar in the
chat, while it is offered for the same member in the members list.
- redeem errors are typed (CEBadgeRedeemError) instead of matched by text in the apps
- service timeout (A_SERVICE) decodes in the apps and offers Retry via the existing retry alert
- unexpected redeem errors show the error itself instead of a generic message
- service error codes are a typed enum in the apps (BadgeServiceErrorCode)
- CRBadgeRedeemed returns badge state, so the apps skip a second round-trip after redeem
- setBadgeAlertAcked is scoped to user_id
- badgeChanged updates non-active profiles, so other profiles' badges don't go stale
- pitch banner is not shown to a profile that already has a badge
- one badgeTypeName per platform, used by the badge screen and the badge info alert
- BadgeAlertKind and BadgeAlertPrice decode via standard JSON, no custom decoders
- dead "Support ended" title branch removed from Your Badge view
- kotlin: users from badge responses carry remoteHostId
- kotlin: redeem code field keeps the IME's cursor and composition state
- kotlin: "Get your code" shown in all flavours
- kotlin: "Don't show again" -> "Dismiss", matching iOS
- kotlin: parseBadgeCode moved next to its FFI in platform/Core.kt
- kotlin: BadgesView no longer cross-fades on badge state updates
- kotlin: section title not uppercased
- ios: redeem code field parses once per change
- ios: A_SERVICE rejected reason is not decoded
- CLI: "cannot redeem badge code: ..." with the source of the error
- comments clarified
* ios, android, desktop: do not add loaded items of another chat to the opened chat
apiLoadMessages could apply a loaded page to a chat it was not loaded for,
mixing messages of two chats in one list:
- the chat id was not checked at all for .last pagination, and the loading
coroutine is not cancelled when the chat is closed (PreloadItems wraps it
into NonCancellable, apiFindMessages runs in a scope of its own), so a .last
page loaded for the previously opened chat was appended to the items of the
chat opened while it was loading;
- for the other paginations the chat id was checked before the items were
loaded, but they are applied on the main thread after a dispatch, so the chat
could be switched in between.
Check the chat id (and the remote host id in kotlin) right before the items are
applied, in every pagination except .initial and opening around item that set
chat id themselves. Empty chatItems remain allowed for .last that is used for
searching.
* android, desktop: test that loaded items of another chat are not added to the opened chat
Reproduces the bug deterministically, without depending on the timing of the
race: processLoadedChat is called with a page loaded for another chat while the
opened chat has its own items.
Without the fix the items of the two chats end up in one list:
lastPageLoadedForAnotherChatIsNotAddedToOpenedChat
expected:<[101, 102]> but was:<[101, 102, 201, 202]>
beforePageLoadedForAnotherChatIsNotAddedToOpenedChat
expected:<[101, 102]> but was:<[201, 202, 101, 102]>
aroundPageLoadedForAnotherChatIsNotAddedToOpenedChat
expected:<[101, 102]> but was:<[101, 102, 201, 202]>
* desktop: fix rotated video squashed on playback and preview rotated twice
vlc applies the display matrix before a frame reaches the vmem callback, so the
buffer has to be requested with the sides swapped for the transposed orientations,
and the snapshot must not be rotated again by hand. Read the snapshot on the event
thread, where the render callback writes it, and draw the inline playback surface
with FillWidth so a video narrower than the item fills it like its preview does.
Bound the requested buffer: the size comes from a received file, so it is capped by
area, cannot be zero, and a frame that does not fill the bitmap is dropped.
* desktop: harden the video frame path against crafted files
Only transpose the buffer for the track's own sides - the size libvlc passes is
already rotated, so swapping it would recreate the squash for a file declaring a
rotation with a zero-sized track. Copy the frame inside the render callback, on
vlc's thread, where the native buffer is guaranteed to exist, and hand only the
copy to the event thread. Drop a frame rendered with a format the bitmap was not
sized by, or arriving before any buffer was allocated. Divide the pixel budget by
a side pinned at 1 instead of scaling both sides, so a 2000000000x1 declaration
cannot take 45 times the budget. Publish the bitmap only when skia took the
pixels, size the copy after a rewind, and log a failed snapshot conversion
instead of throwing it into callers that have no handler for it.
* ui: show role (if exists) and remove "new" in open channel/group alerts
* restore removed strings
* info color
* docs
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>