mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-22 15:22:07 +00:00
android, desktop: open group member profile without waiting for the core (#7485)
* 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>
This commit is contained in:
1 parent
3ed4511e80
commit
67c619acff
6 files changed
+284
-36
No files matched your search
+2
-1
@@ -1205,12 +1205,13 @@ fun SynchronizeConnectionButtonForce(syncConnectionForce: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
|
||||
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit, disabled: Boolean = false) {
|
||||
SettingsActionItem(
|
||||
if (contactVerified) painterResource(MR.images.ic_verified_user) else painterResource(MR.images.ic_shield),
|
||||
stringResource(if (contactVerified) MR.strings.view_security_code else MR.strings.verify_security_code),
|
||||
click = onClick,
|
||||
iconColor = MaterialTheme.colors.secondary,
|
||||
disabled = disabled,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+12
-9
@@ -499,6 +499,15 @@ fun ChatView(
|
||||
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
|
||||
hideKeyboard(view)
|
||||
groupMembersJob.cancel()
|
||||
val connStats = mutableStateOf<ConnectionStats?>(null)
|
||||
val connectionCode = mutableStateOf<String?>(null)
|
||||
val connectionLoaded = mutableStateOf(false)
|
||||
if (chatsCtx.secondaryContextFilter == null) {
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close ->
|
||||
GroupMemberInfoView(chatRh, groupInfo, member, scrollToItemId, connStats, connectionCode, connectionLoaded, chatModel, openedFromSupportChat = false, close = close, closeAll = close)
|
||||
}
|
||||
groupMembersJob = scope.launch(Dispatchers.Default) {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
@@ -513,15 +522,9 @@ fun ChatView(
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.upsertGroupMember(chatRh, groupInfo, updatedMember)
|
||||
}
|
||||
|
||||
if (chatsCtx.secondaryContextFilter == null) {
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(chatRh, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, close = close, closeAll = close)
|
||||
}
|
||||
}
|
||||
connStats.value = stats
|
||||
connectionCode.value = code
|
||||
connectionLoaded.value = true
|
||||
}
|
||||
},
|
||||
loadMessages = { chatId, pagination, visibleItemIndexes ->
|
||||
|
||||
+12
-8
@@ -123,6 +123,15 @@ fun ModalData.GroupChatInfoView(
|
||||
}
|
||||
},
|
||||
showMemberInfo = { member, groupRelay ->
|
||||
val connStats = mutableStateOf<ConnectionStats?>(null)
|
||||
val connectionCode = mutableStateOf<String?>(null)
|
||||
val connectionLoaded = mutableStateOf(false)
|
||||
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { closeCurrent ->
|
||||
GroupMemberInfoView(rhId, groupInfo, member, scrollToItemId, connStats, connectionCode, connectionLoaded, chatModel, openedFromSupportChat = false, groupRelay = groupRelay, close = closeCurrent) {
|
||||
closeCurrent()
|
||||
close()
|
||||
}
|
||||
}
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
@@ -132,14 +141,9 @@ fun ModalData.GroupChatInfoView(
|
||||
} else {
|
||||
member to null
|
||||
}
|
||||
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { closeCurrent ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, groupRelay = groupRelay, close = closeCurrent) {
|
||||
closeCurrent()
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
connStats.value = stats
|
||||
connectionCode.value = code
|
||||
connectionLoaded.value = true
|
||||
}
|
||||
},
|
||||
editGroupProfile = {
|
||||
|
||||
+33
-10
@@ -45,10 +45,11 @@ import kotlinx.coroutines.*
|
||||
fun GroupMemberInfoView(
|
||||
rhId: Long?,
|
||||
groupInfo: GroupInfo,
|
||||
member: GroupMember,
|
||||
groupMember: GroupMember,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
connectionStats: ConnectionStats?,
|
||||
connectionCode: String?,
|
||||
connStats: MutableState<ConnectionStats?>,
|
||||
connectionCode: State<String?>,
|
||||
connectionLoaded: State<Boolean>,
|
||||
chatModel: ChatModel,
|
||||
openedFromSupportChat: Boolean,
|
||||
groupRelay: GroupRelay? = null,
|
||||
@@ -60,7 +61,10 @@ fun GroupMemberInfoView(
|
||||
}
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId }
|
||||
val connStats = remember { mutableStateOf(connectionStats) }
|
||||
// the passed member is shown until the loaded one is added to the model, so that the profile opens without waiting for the core
|
||||
val member = remember(groupMember.groupMemberId) {
|
||||
derivedStateOf { chatModel.getGroupMember(groupMember.groupMemberId) ?: groupMember }
|
||||
}.value
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
var progressIndicator by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -91,7 +95,8 @@ fun GroupMemberInfoView(
|
||||
connStats,
|
||||
newRole,
|
||||
developerTools,
|
||||
connectionCode,
|
||||
connectionCode.value,
|
||||
connectionLoaded.value,
|
||||
groupRelay = groupRelay,
|
||||
getContactChat = { chatModel.getContactChat(it) },
|
||||
openDirectChat = { contactId ->
|
||||
@@ -101,6 +106,7 @@ fun GroupMemberInfoView(
|
||||
}
|
||||
},
|
||||
createMemberContact = {
|
||||
val connectionStats = connStats.value
|
||||
if (member.sendMsgEnabled) {
|
||||
withBGApi {
|
||||
progressIndicator = true
|
||||
@@ -209,7 +215,7 @@ fun GroupMemberInfoView(
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
VerifyCodeView(
|
||||
mem.displayName,
|
||||
connectionCode,
|
||||
connectionCode.value,
|
||||
mem.verified,
|
||||
verify = { code ->
|
||||
chatModel.controller.apiVerifyGroupMember(rhId, mem.groupId, mem.groupMemberId, code)?.let { r ->
|
||||
@@ -368,6 +374,7 @@ fun GroupMemberInfoLayout(
|
||||
newRole: MutableState<GroupMemberRole>,
|
||||
developerTools: Boolean,
|
||||
connectionCode: String?,
|
||||
connectionLoaded: Boolean,
|
||||
groupRelay: GroupRelay? = null,
|
||||
getContactChat: (Long) -> Chat?,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
@@ -538,22 +545,23 @@ fun GroupMemberInfoLayout(
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
val memberConnected = member.memberActive || (groupInfo.useRelays && member.memberCurrent)
|
||||
val showMemberSupportChat = !openedFromSupportChat &&
|
||||
groupInfo.membership.memberRole >= GroupMemberRole.Moderator &&
|
||||
member.memberRole != GroupMemberRole.Relay &&
|
||||
((groupInfo.fullGroupPreferences.support.on && member.memberRole < GroupMemberRole.Moderator)
|
||||
|| member.supportChat != null)
|
||||
val canVerifyCode = connectionCode != null && member.memberRole != GroupMemberRole.Relay
|
||||
// the same condition decides whether the code is requested, so the row is shown disabled until it arrives
|
||||
val canVerifyCode = member.memberRole != GroupMemberRole.Relay && (connectionCode != null || !connectionLoaded)
|
||||
val canSyncConn = cStats != null && cStats.ratchetSyncAllowed
|
||||
|
||||
if ((member.memberActive || (groupInfo.useRelays && member.memberCurrent))
|
||||
&& (showMemberSupportChat || canVerifyCode || canSyncConn)) {
|
||||
if (memberConnected && (showMemberSupportChat || canVerifyCode || canSyncConn)) {
|
||||
SectionView {
|
||||
if (showMemberSupportChat) {
|
||||
SupportChatButton()
|
||||
}
|
||||
if (canVerifyCode) {
|
||||
VerifyCodeButton(member.verified, verifyClicked)
|
||||
VerifyCodeButton(member.verified, verifyClicked, disabled = connectionCode == null)
|
||||
}
|
||||
if (canSyncConn) {
|
||||
SynchronizeConnectionButton(syncMemberConnection)
|
||||
@@ -655,6 +663,20 @@ fun GroupMemberInfoLayout(
|
||||
SimplexServers(stringResource(MR.strings.sending_via), sndServers)
|
||||
}
|
||||
}
|
||||
} else if (!connectionLoaded && memberConnected) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
|
||||
// the status is set from the same receiving queues as the servers below, so it is reserved with them
|
||||
SectionItemView {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(stringResource(MR.strings.network_status))
|
||||
Icon(painterResource(MR.images.ic_info), stringResource(MR.strings.network_status), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
SwitchAddressButton(disabled = true, switchAddress = {})
|
||||
SimplexServers(stringResource(MR.strings.receiving_via), emptyList())
|
||||
SimplexServers(stringResource(MR.strings.sending_via), emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
val connFailedErr = member.activeConn?.connFailedErr
|
||||
@@ -1117,6 +1139,7 @@ fun PreviewGroupMemberInfoLayout() {
|
||||
newRole = remember { mutableStateOf(GroupMemberRole.Member) },
|
||||
developerTools = false,
|
||||
connectionCode = "123",
|
||||
connectionLoaded = true,
|
||||
getContactChat = { Chat.sampleData },
|
||||
openDirectChat = {},
|
||||
createMemberContact = {},
|
||||
|
||||
+12
-8
@@ -76,6 +76,15 @@ fun MemberSupportChatAppBar(
|
||||
navigationButton = { NavigationButtonBack(onBackClicked) },
|
||||
title = { MemberSupportChatToolbarTitle(scopeMember_) },
|
||||
onTitleClick = {
|
||||
val connStats = mutableStateOf<ConnectionStats?>(null)
|
||||
val connectionCode = mutableStateOf<String?>(null)
|
||||
val connectionLoaded = mutableStateOf(false)
|
||||
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { closeCurrent ->
|
||||
GroupMemberInfoView(rhId, groupInfo, scopeMember_, scrollToItemId, connStats, connectionCode, connectionLoaded, chatModel, openedFromSupportChat = true, close = closeCurrent) {
|
||||
closeCurrent()
|
||||
close()
|
||||
}
|
||||
}
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, scopeMember_.groupMemberId)
|
||||
val stats = r?.second
|
||||
@@ -85,14 +94,9 @@ fun MemberSupportChatAppBar(
|
||||
} else {
|
||||
null
|
||||
}
|
||||
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { closeCurrent ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(scopeMember_.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = true, close = closeCurrent) {
|
||||
closeCurrent()
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
connStats.value = stats
|
||||
connectionCode.value = code
|
||||
connectionLoaded.value = true
|
||||
}
|
||||
},
|
||||
onTop = !oneHandUI.value || !chatBottomBar.value,
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# Open Member Profile Without Waiting for the Core
|
||||
|
||||
## Context
|
||||
|
||||
`#7388` removed `apiListMembers` from the member profile tap, which was O(group
|
||||
size) and took seconds in a group with 10000 members. It did not remove the
|
||||
dependency on the core.
|
||||
|
||||
`showMemberInfo` (ChatView.kt:499) still awaited two calls before the modal was
|
||||
created:
|
||||
|
||||
```kotlin
|
||||
val r = chatModel.controller.apiGroupMemberInfo(...) // awaited
|
||||
val (updatedMember, code) = if (...) {
|
||||
val memCode = chatModel.controller.apiGetGroupMemberCode(...) // awaited
|
||||
...
|
||||
}
|
||||
...
|
||||
ModalManager.end.showModalCloseable(...) { ... } // only now
|
||||
```
|
||||
|
||||
Both are single-row queries, 1-2 ms when the core is idle, which is why the tap
|
||||
looks instant in testing. 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
|
||||
profile opens at the speed of the core rather than the speed of the UI.
|
||||
|
||||
The same wait is in `GroupChatInfoView.kt:125` and `MemberSupportChatView.kt:78`.
|
||||
|
||||
## Root Cause
|
||||
|
||||
The modal was created inside the coroutine, after both awaits, and the loaded
|
||||
values were passed into the view as plain parameters:
|
||||
|
||||
```kotlin
|
||||
fun GroupMemberInfoView(
|
||||
...
|
||||
connectionStats: ConnectionStats?,
|
||||
connectionCode: String?,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
A plain parameter can only be supplied by a caller that has already waited.
|
||||
|
||||
Each call site also resolved the member through the model and rendered nothing
|
||||
when it was absent:
|
||||
|
||||
```kotlin
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> ... }
|
||||
```
|
||||
|
||||
In a large group where members were never loaded, that is an empty card - which
|
||||
is why `#7388` had to write the member into the model *before* showing the modal.
|
||||
|
||||
## Solution
|
||||
|
||||
Open the modal first and let the loaded values arrive afterwards. The two
|
||||
parameters become state the caller creates at the tap and fills in when the
|
||||
queries return:
|
||||
|
||||
```kotlin
|
||||
val connStats = mutableStateOf<ConnectionStats?>(null)
|
||||
val connectionCode = mutableStateOf<String?>(null)
|
||||
ModalManager.end.showModalCloseable(...) { close ->
|
||||
GroupMemberInfoView(chatRh, groupInfo, member, scrollToItemId, connStats, connectionCode, ...)
|
||||
}
|
||||
groupMembersJob = scope.launch(Dispatchers.Default) {
|
||||
...unchanged...
|
||||
connStats.value = r?.second
|
||||
connectionCode.value = code
|
||||
}
|
||||
```
|
||||
|
||||
The card is shown from the member that is already known at the tap - from the
|
||||
chat item, or from the members list - with the model's copy preferred once it is
|
||||
there:
|
||||
|
||||
```kotlin
|
||||
val member = remember(groupMember.groupMemberId) {
|
||||
derivedStateOf { chatModel.getGroupMember(groupMember.groupMemberId) ?: groupMember }
|
||||
}.value
|
||||
```
|
||||
|
||||
The `?: groupMember` fallback is what removes the dependency: the card no longer
|
||||
needs the model to contain the member, so nothing has to happen before the modal
|
||||
opens.
|
||||
|
||||
## Technical Design
|
||||
|
||||
### The state is created at the tap, not in the view
|
||||
|
||||
`ModalManager.showInView` drives `AnimatedContent` from `modalCount` alone
|
||||
(ModalView.kt:207), so the card is disposed while a sub-screen is open and
|
||||
re-composed on return. State held by `remember` inside the view would lose the
|
||||
loaded values there, and the rows would disappear. A `MutableState` created in
|
||||
the click handler is captured by the modal lambda and outlives every
|
||||
recomposition of the card, with no storage of its own.
|
||||
|
||||
### The loaded code cannot come from the model
|
||||
|
||||
`APIGetGroupMemberCode` returns the live code beside the member
|
||||
(Commands.hs:2047); the member it returns carries only the verification record,
|
||||
which the core *clears* when the code no longer matches. So the code has to be
|
||||
held by the UI - it cannot be read back off `member.activeConn`.
|
||||
|
||||
### The rows are reserved, not gated and not inserted
|
||||
|
||||
iOS wraps everything below the action buttons in `if connectionLoaded`
|
||||
(GroupMemberInfoView.swift:123-288) - the action section, Address, Member,
|
||||
Servers, Connection failed and For console. Until both queries return its card is
|
||||
a header and three buttons over blank space. That is tolerable when the core
|
||||
answers in 1-2 ms; it is the wrong trade for the case this change exists for,
|
||||
where the core is busy and the wait is what the user sees.
|
||||
|
||||
Letting each row appear as its data arrives is no better: the card is complete
|
||||
from the first frame but grows under the finger.
|
||||
|
||||
So the rows that depend on the load are rendered from the first frame in their
|
||||
final positions, disabled, and enabled in place when the data arrives:
|
||||
|
||||
```kotlin
|
||||
val canVerifyCode = member.memberRole != GroupMemberRole.Relay && (connectionCode != null || !connectionLoaded)
|
||||
...
|
||||
VerifyCodeButton(member.verified, verifyClicked, disabled = connectionCode == null)
|
||||
```
|
||||
|
||||
`SectionItemView` already drops the `clickable` modifier when `disabled`
|
||||
(Section.kt:213), so a disabled row is inert and identical in size to the enabled
|
||||
one - nothing moves when it is enabled.
|
||||
|
||||
Only rows that can be predicted locally are reserved:
|
||||
|
||||
- **Verify security code.** The enclosing condition is already
|
||||
`memberActive || (useRelays && memberCurrent)`, so inside it the loader's
|
||||
`getCode` reduces to `memberRole != Relay` - known from the member at the tap.
|
||||
- **Servers.** Reserved as a whole section with a disabled "Change receiving
|
||||
address" and empty Receiving/Sending rows, for a member the same condition says
|
||||
is connected.
|
||||
- **Fix connection is not reserved.** `canSyncConn` needs
|
||||
`cStats.ratchetSyncAllowed`, which is false except during ratchet desync, so a
|
||||
placeholder for it would disappear on nearly every open - the same jump in the
|
||||
opposite direction.
|
||||
|
||||
A third state, `connectionLoaded`, is created at the tap beside the other two and
|
||||
set when the queries return, including on failure. Without it a failed load would
|
||||
leave the row disabled forever, because "no code yet" and "no code at all" look
|
||||
the same.
|
||||
|
||||
Network status is reserved with them. It looks like agent state that cannot be
|
||||
predicted, but `connSubStatus` (simplexmq Agent.hs:2736) returns `Just` whenever
|
||||
`rcvQueuesInfo` is non-empty and `Nothing` only when it is empty - the same list
|
||||
that decides whether "Receiving via" is rendered. The two rows appear together,
|
||||
so reserving one and not the other is what leaves a gap.
|
||||
|
||||
The remaining shift is "Abort changing address", which renders only while
|
||||
`rcvSwitchStatus != null` on a receiving queue. That is a switch the user
|
||||
started, so it is absent in normal use and reserving it would add a row that
|
||||
disappears on nearly every open.
|
||||
|
||||
iOS needs its gate for a reason Kotlin does not have: `newRole` is `@State`
|
||||
initialised to a placeholder `.member` and corrected inside `.task`
|
||||
(GroupMemberInfoView.swift:30, :297), so the role picker must not render before
|
||||
that runs. Kotlin seeds it from the member inline -
|
||||
`remember { mutableStateOf(member.memberRole) }` - and is correct on the first
|
||||
frame.
|
||||
|
||||
### The chat-change guard is kept
|
||||
|
||||
`#7388` added `chatModel.chatId.value != groupInfo.id` so that a load finishing
|
||||
after the chat changed does not write to the model. The modal now opens before
|
||||
the guard is reached, and `KeyChangeEffect` (:56) closes the card on chat change
|
||||
as before, so the guard is left exactly as it was.
|
||||
|
||||
`groupMembersJob.cancel()` is kept in the tap handler: `info` (ChatView.kt:390)
|
||||
preloads members *before* showing its modal, so without the cancel a pending chat
|
||||
info load would open its modal on top of the member card.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The card renders from the member known at the tap, so it can be marginally
|
||||
staler for the duration of the load. iOS has the same window - it renders the
|
||||
header before `connectionLoaded`.
|
||||
- "Verify security code" and the Servers section are shown disabled until the
|
||||
queries return, so the card does not change height as they arrive. This is a
|
||||
third behaviour, matching neither the old Kotlin (rows inserted as data landed)
|
||||
nor iOS (whole body hidden until loaded); porting it to iOS would be the way to
|
||||
converge them.
|
||||
- "Fix connection" still appears on arrival when the ratchet needs syncing. It is
|
||||
rare enough that reserving space for it would move the card more often than it
|
||||
saves.
|
||||
- "Send message" for a member with `!sendMsgEnabled` does nothing if tapped
|
||||
before the stats arrive. iOS is identical: `createMemberContactButton` has the
|
||||
same `else if let connStats` with no else, and renders the action buttons
|
||||
before `connectionLoaded`.
|
||||
- The modal is now opened from the click handler on the main thread instead of
|
||||
from `Dispatchers.Default` / `withBGApi`. All three callers are UI click
|
||||
handlers, so this is strictly safer - `modalViews` is a plain `ArrayList`.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- **Collapsing the three copies of the fetch.** The same two queries and the same
|
||||
condition are still written out at all three call sites. That duplication has
|
||||
already drifted once - `#7255` widened the condition for channel members in two
|
||||
of the three and in iOS, and `ChatView.kt` was left behind until `#7486` - and
|
||||
moving the load into the view, as iOS has done since `#5008`, would remove it.
|
||||
It is a restructure rather than a fix, so it belongs in its own change.
|
||||
- Message *Info* (ChatView.kt:706) still loads all members; it needs them to
|
||||
resolve delivery recipients. A narrower core API would be required.
|
||||
- Opening a second member's profile while one is already open does not switch the
|
||||
card: `closeModals()` sets `modalCount` to 0 and `showCustomModal` back to 1, so
|
||||
`AnimatedContent`'s target never changes. The close and open are back to back in
|
||||
both the old and the new code, so this is neither introduced nor worsened.
|
||||
Reference in new issue
Block a user