mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 00:02:28 +00:00
ui: open a page once when its button is clicked again or double-clicked (#7725)
* desktop: open a pending connection's "Learn more" and "Incognito" pages once
On desktop the connection view stays clickable in the center pane, so each
click on these rows pushed another modal onto the right pane. Close the
right-pane modals first, as ChatView does when the connection opens. The page
is closed and shown again in one event, so its content is keyed by its
ModalData to start fresh.
Desktop only: on Android the view can itself be the open modal of the shared
manager ("Set contact name"), and the new page already covers the row.
Fixes #6270
* ui: open a modal once when its button is double-clicked
A modal slides in over 250 ms, and until it covers the button that opened it the button still takes clicks, so a double click opened two copies (left-pane pages and most stacked center and right-pane pages on desktop, animated modals on Android). When a modal opens, the page it covers draws an empty pointerInput box over itself while it slides out; when a pane's first modal slides in, the empty page sliding out covers the view under the pane. The page sliding in already takes clicks, and nothing is blocked when a page closes.
* ui: add isTopModalOpen, which ignores modals that are sliding out
A modal closed with animation stays in modalViews until it has slid out, and isLastModalOpen counts it. A check-then-closeModal() on it would close the modal under it. Uses the same index as isLastModal; isLastModalOpen and its callers are unchanged.
* desktop: don't open a second badges page or profile form
These buttons stay clickable on desktop while their page is shown in another pane, so each click opened another copy:
- "Support SimpleX" in the chat's large-file and badge alerts (Badges on the right): the alerts skip while Badges is open, on desktop only, as on Android the page covers the button;
- "+ Add profile" in Your chat profiles and "Create chat profile" in the picker (form in the center): skip while the form is on top, keeping what was typed, before asking for authentication; the picker row still hides the picker.
* desktop: replace a page of the same kind on top instead of stacking another
These buttons stay clickable on desktop while their page is shown in another pane, so each click pushed another copy:
- the chat banner's bio "Read more" (right pane);
- a markdown file clicked in a support chat (viewer in the center);
- message "Info" in the main chat while a support or reports chat is open on the right.
Their content depends on what was clicked and the pane can hold other pages (a support chat), so a page of the same kind on top is replaced. The new page is drawn where the old one was, so its content is keyed by its ModalData to start fresh. The markdown file and Info load on a background thread; their close and show now run on the main thread. The markdown check is desktop only, as on Android the viewer covers its file once it has loaded.
* plans: a page opens again on every click of its button (#6270)
* plans: D2 is valid at 80 ms only
This commit is contained in:
1 parent
3126d9b544
commit
87e3f8c3f6
10 files changed
+156
-46
No files matched your search
+23
-19
@@ -723,35 +723,39 @@ fun ChatView(
|
||||
ciInfo
|
||||
}
|
||||
groupMembersJob.cancel()
|
||||
groupMembersJob = scope.launch(Dispatchers.Default) {
|
||||
var initialCiInfo = loadChatItemInfo() ?: return@launch
|
||||
groupMembersJob = scope.launch {
|
||||
var initialCiInfo = withContext(Dispatchers.Default) { loadChatItemInfo() } ?: return@launch
|
||||
if (!ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
|
||||
ModalManager.end.closeModals()
|
||||
} else if (ModalManager.end.isTopModalOpen(ModalViewId.CHAT_ITEM_INFO)) {
|
||||
ModalManager.end.closeModal()
|
||||
}
|
||||
ModalManager.end.showModalCloseable(endButtons = {
|
||||
ModalManager.end.showModalCloseable(id = ModalViewId.CHAT_ITEM_INFO, endButtons = {
|
||||
ShareButton {
|
||||
clipboard.shareText(itemInfoShareText(chatModel, initialCiInfo.first, initialCiInfo.second, chatModel.controller.appPrefs.developerTools.get()))
|
||||
}
|
||||
}) { close ->
|
||||
var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) }
|
||||
val (item, info) = ciInfo
|
||||
ChatItemInfoView(chatRh, item, info, devTools = chatModel.controller.appPrefs.developerTools.get(), chatInfo)
|
||||
LaunchedEffect(cItem.id) {
|
||||
withContext(Dispatchers.Default) {
|
||||
for (msg in controller.messagesChannel) {
|
||||
if (msg.rhId == chatRh &&
|
||||
msg is API.Result &&
|
||||
msg.res is CR.ChatItemsStatusesUpdated &&
|
||||
msg.res.chatItems.any { it.chatItem.id == cItem.id }
|
||||
) {
|
||||
ciInfo = loadChatItemInfo() ?: return@withContext
|
||||
initialCiInfo = ciInfo
|
||||
key(this) {
|
||||
var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) }
|
||||
val (item, info) = ciInfo
|
||||
ChatItemInfoView(chatRh, item, info, devTools = chatModel.controller.appPrefs.developerTools.get(), chatInfo)
|
||||
LaunchedEffect(cItem.id) {
|
||||
withContext(Dispatchers.Default) {
|
||||
for (msg in controller.messagesChannel) {
|
||||
if (msg.rhId == chatRh &&
|
||||
msg is API.Result &&
|
||||
msg.res is CR.ChatItemsStatusesUpdated &&
|
||||
msg.res.chatItems.any { it.chatItem.id == cItem.id }
|
||||
) {
|
||||
ciInfo = loadChatItemInfo() ?: return@withContext
|
||||
initialCiInfo = ciInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
close()
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -333,7 +333,7 @@ fun showLargeFileAlert(fileSize: Long, incognito: Boolean, senderProfile: LocalP
|
||||
text = text,
|
||||
confirmText = generalGetString(MR.strings.ok),
|
||||
dismissText = generalGetString(MR.strings.badges_support_simplex_title),
|
||||
onDismiss = ::openBadgesView
|
||||
onDismiss = { if (!appPlatform.isDesktop || !ModalManager.end.hasModalOpen(ModalViewId.BADGES)) openBadgesView() }
|
||||
)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(title, text)
|
||||
|
||||
+8
-5
@@ -96,11 +96,14 @@ fun showMarkdownFile(file: CIFile) {
|
||||
formatted,
|
||||
bidiControlChar.containsMatchIn(text)
|
||||
)
|
||||
ModalManager.center.showModal(endButtons = {
|
||||
if (formatted != null) MarkdownViewSwitch()
|
||||
MarkdownFileSaveButton(file, bytes)
|
||||
}) {
|
||||
MarkdownFileView(file.fileName, document)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (appPlatform.isDesktop && ModalManager.center.isTopModalOpen(ModalViewId.MARKDOWN_FILE)) ModalManager.center.closeModal()
|
||||
ModalManager.center.showModal(id = ModalViewId.MARKDOWN_FILE, endButtons = {
|
||||
if (formatted != null) MarkdownViewSwitch()
|
||||
MarkdownFileSaveButton(file, bytes)
|
||||
}) {
|
||||
key(this) { MarkdownFileView(file.fileName, document) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-15
@@ -65,21 +65,24 @@ private fun openMarkdownModal(modal: Format.Modal) {
|
||||
}
|
||||
|
||||
private fun showFullProfileDescription(description: String) {
|
||||
ModalManager.end.showModalCloseable { _ ->
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(generalGetString(MR.strings.profile_description__field))
|
||||
val parsed = parseToMarkdownText(description)
|
||||
MarkdownText(
|
||||
description,
|
||||
parsed?.formattedText,
|
||||
parsed?.markdownBlocks,
|
||||
toggleSecrets = true,
|
||||
style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground, lineHeight = 22.sp),
|
||||
uriHandler = LocalUriHandler.current,
|
||||
linkMode = chatModel.simplexLinkMode.value,
|
||||
modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING),
|
||||
blockLayout = true,
|
||||
)
|
||||
if (ModalManager.end.isTopModalOpen(ModalViewId.PROFILE_DESCRIPTION)) ModalManager.end.closeModal()
|
||||
ModalManager.end.showModalCloseable(id = ModalViewId.PROFILE_DESCRIPTION) { _ ->
|
||||
key(this) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(generalGetString(MR.strings.profile_description__field))
|
||||
val parsed = parseToMarkdownText(description)
|
||||
MarkdownText(
|
||||
description,
|
||||
parsed?.formattedText,
|
||||
parsed?.markdownBlocks,
|
||||
toggleSecrets = true,
|
||||
style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground, lineHeight = 22.sp),
|
||||
uriHandler = LocalUriHandler.current,
|
||||
linkMode = chatModel.simplexLinkMode.value,
|
||||
modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING),
|
||||
blockLayout = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -274,8 +274,12 @@ fun UserPicker(
|
||||
painterResource(MR.images.ic_manage_accounts),
|
||||
generalGetString(MR.strings.create_chat_profile),
|
||||
{
|
||||
if (ModalManager.center.isTopModalOpen(ModalViewId.CREATE_PROFILE)) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
return@UserPickerOptionRow
|
||||
}
|
||||
doWithAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) {
|
||||
ModalManager.center.showModalCloseable { close ->
|
||||
ModalManager.center.showModalCloseable(id = ModalViewId.CREATE_PROFILE) { close ->
|
||||
LaunchedEffect(Unit) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
}
|
||||
|
||||
+1
-1
@@ -252,7 +252,7 @@ fun showBadgeInfoAlert(name: String, badge: LocalBadge, uriHandler: UriHandler)
|
||||
text = supports,
|
||||
confirmText = generalGetString(MR.strings.ok),
|
||||
dismissText = generalGetString(MR.strings.badges_support_simplex_title),
|
||||
onDismiss = ::openBadgesView
|
||||
onDismiss = { if (!appPlatform.isDesktop || !ModalManager.end.hasModalOpen(ModalViewId.BADGES)) openBadgesView() }
|
||||
)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(title = title, text = supports)
|
||||
|
||||
+12
-1
@@ -9,6 +9,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import LocalCardScreen
|
||||
@@ -93,7 +94,11 @@ enum class ModalViewId {
|
||||
CONTEXT_USER_PICKER_INCOGNITO,
|
||||
BADGE_LINK,
|
||||
BADGE_REDEEM_CODE,
|
||||
BADGES
|
||||
BADGES,
|
||||
CREATE_PROFILE,
|
||||
PROFILE_DESCRIPTION,
|
||||
MARKDOWN_FILE,
|
||||
CHAT_ITEM_INFO
|
||||
}
|
||||
|
||||
class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
@@ -118,6 +123,8 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
|
||||
fun isLastModalOpen(id: ModalViewId): Boolean = modalViews.lastOrNull()?.id == id
|
||||
|
||||
fun isTopModalOpen(id: ModalViewId): Boolean = modalViews.getOrNull(modalViews.size - toRemove.size - 1)?.id == id
|
||||
|
||||
fun isLastModal(data: ModalData): Boolean = modalViews.getOrNull(modalViews.size - toRemove.size - 1)?.data === data
|
||||
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, forceAnimated: Boolean = false, cardScreen: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
|
||||
@@ -224,6 +231,10 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
if (toRemove.isNotEmpty() && it == modalCount.value && transition.currentState == EnterExitState.Visible && !transition.isRunning) {
|
||||
runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } }
|
||||
}
|
||||
// Until the opening modal covers the page that opened it, or the view under the first modal, a second click there would open it again
|
||||
if (transition.targetState == EnterExitState.PostExit && it < modalCount.value) {
|
||||
Box(Modifier.fillMaxSize().pointerInput(Unit) {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -66,8 +66,9 @@ fun ContactConnectionInfoView(
|
||||
deleteConnection = { deleteContactConnectionAlert(rhId, contactConnection, chatModel, close) },
|
||||
onLocalAliasChanged = { setContactAlias(rhId, contactConnection, it, chatModel) },
|
||||
learnMore = {
|
||||
if (appPlatform.isDesktop) ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable { close ->
|
||||
AddContactLearnMore(close)
|
||||
key(this) { AddContactLearnMore(close) }
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -90,7 +91,10 @@ private fun ContactConnectionInfoLayout(
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_theater_comedy_filled),
|
||||
text = null,
|
||||
click = { ModalManager.end.showModal { IncognitoView() } },
|
||||
click = {
|
||||
if (appPlatform.isDesktop) ModalManager.end.closeModals()
|
||||
ModalManager.end.showModal { key(this) { IncognitoView() } }
|
||||
},
|
||||
iconColor = Indigo,
|
||||
extraPadding = false
|
||||
) {
|
||||
|
||||
+2
-1
@@ -47,8 +47,9 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
|
||||
showHiddenProfilesNotice = m.controller.appPrefs.showHiddenProfilesNotice,
|
||||
visibleUsersCount = visibleUsersCount(m),
|
||||
addUser = {
|
||||
if (ModalManager.center.isTopModalOpen(ModalViewId.CREATE_PROFILE)) return@UserProfilesLayout
|
||||
withAuth {
|
||||
ModalManager.center.showModalCloseable { close ->
|
||||
ModalManager.center.showModalCloseable(id = ModalViewId.CREATE_PROFILE) { close ->
|
||||
CreateProfile(m, close)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# A page opens again on every click of the button that opened it
|
||||
|
||||
Fixes #6270.
|
||||
|
||||
## Problem
|
||||
|
||||
Issue #6270 reports one case: on desktop (Linux), clicking "Learn more" repeatedly in a pending connection opens the page again on every click. Each copy then needs its own Back. The same bug has two causes, and both occur across the app:
|
||||
|
||||
1. **The button stays clickable in another pane.** On desktop the window has a left pane (chat list, `ModalManager.start`), a center pane (chat or `ModalManager.center`) and a right pane (`ModalManager.end`). Each pane draws only the top modal of its own stack. When a button in one pane opens a page in another, the button stays visible and clickable, and every click pushes another copy.
|
||||
2. **A fast double click during the slide-in.** A modal slides in over 250 ms. Until it covers the button that opened it, the button still takes clicks, so a double click opens two copies. Pages slide in on Android and in the desktop left pane; in the desktop center and right panes the first page never slides, the third and later do, and the second does only after an earlier page there has slid out.
|
||||
|
||||
Reproduced on the master prebuild (a932c82), R8 on the stable prebuild (master has the same code):
|
||||
|
||||
| # | Steps | Clicks | Backs needed |
|
||||
|---|---|---|---|
|
||||
| R1 | Pending connection: "Learn more" or "Incognito" | 3 | 3 |
|
||||
| R2 | Your chat profiles: "+ Add profile" (form opens in the center) | 3 | 3 |
|
||||
| R3 | "Chat with admins" open on the right (a reports chat opens the same way; not tried); in the group chat, message menu → "Info" | 3 | 3 |
|
||||
| R4 | Direct chat banner: profile bio "Read more" | 3 | 3 |
|
||||
| R5 | Attach a file over 1 GB; "Large file" alert → "Support SimpleX" | 3 | 3 |
|
||||
| R6 | `.md` file clicked in a support chat on the right (viewer opens in the center) | 3 | 3 |
|
||||
| R7 | What's New opened in the center after an update → "Create your address" → "Address settings" | 3 | 3 |
|
||||
| R8 | No profile and no linked mobile: picker → "Create chat profile" | 3 | 3 |
|
||||
| D1 | Settings: double-click "Appearance" (80 ms, 150 ms) | 2 | 2 |
|
||||
| D2 | Settings: double-click "Network & servers" (80 ms) | 2 | 2 |
|
||||
| D3 | User menu: double-click "Your chat profiles" (80 ms) | 2 | 2 |
|
||||
|
||||
Controls that don't stack on master: message "Info" with nothing open on the right, a double click on "Group preferences" when group info is the only page on the right, a double click on the new-chat pencil.
|
||||
|
||||
## Audit
|
||||
|
||||
Every `ModalManager.{start,center,end,fullscreen}.show*` call on master (about 170) was classified by where its button is drawn on desktop and whether that button stays clickable after the page opens. Wrapper functions and every caller of each view were followed. The cases above are all those found. All other call sites are covered by the page they open, guarded already, or open a fullscreen modal, which covers every pane.
|
||||
|
||||
Existing guards: close the target pane first (`ChatListNavLinkView` "Set contact name", the pending-connection branch of `ChatView`), skip if a page with that id is open (`BadgesSupportSimplexView`, `BADGE_REDEEM_CODE`), toggles (the chat info button, the profile picker's incognito info), and the desktop overlay in `DesktopScreen` that closes left-pane pages on a click in the center (it guards the onboarding cards there).
|
||||
|
||||
## Fix
|
||||
|
||||
### Double click: block clicks on the page being covered (`ModalManager.showInView`)
|
||||
|
||||
`AnimatedContent` draws each page in a child with its own enter or exit transition. When a modal opens, the child sliding out (`targetState == PostExit`, below the new count) also draws an empty `pointerInput` box over itself. That child is the page being covered, which is the page that opened the new one when the button is in the same pane, or, for the first modal of a pane, the empty child, whose box covers the chat list or chat under the pane. The rest of the pane is under the page sliding in, whose `Surface` already takes clicks. Nothing is blocked when a page closes, as on master.
|
||||
|
||||
This fixes the double click for every animated modal on both platforms, with no change at the call sites.
|
||||
|
||||
### Buttons in another pane
|
||||
|
||||
Each site uses an existing guard, or "replace" (an on-top check followed by a close and a show), chosen by where its button can be and what the page shows:
|
||||
|
||||
| Site | Guard | Why this one |
|
||||
|---|---|---|
|
||||
| `ContactConnectionInfoView` "Learn more", "Incognito" (R1) | desktop: close the right pane first | The button is never in the right pane on desktop. On Android the view can itself be the open modal ("Set contact name"), so the call is desktop only. |
|
||||
| The large-file and badge alerts' "Support SimpleX" (R5), which call `openBadgesView` | desktop: skip if `BADGES` is open | Badges can also be open under other right-pane pages, so the right pane can't be closed. `BADGES` is already set on this modal. Badges sub-pages stack above it, so the check is "open", not "on top": while an open Badges page is covered by another page, these two buttons do nothing. The check is at the two alerts, not in `openBadgesView`, so the "Larger files, longer" row in message Info, which its Badges page covers, is unchanged. On Android the page covers its button. |
|
||||
| `UserProfilesView` "+ Add profile" (R2), `UserPicker` "Create chat profile" (R8) | skip if `CREATE_PROFILE` is on top | Closing the center would also close What's New when it is open there, and clear `chatModel.secondaryChatsContext` while a support chat is open on the right (`closeModals()` always clears it). Both clicks open the same empty form, so the open form, with anything already typed, is kept. Both check before asking for authentication, and the picker row also hides the picker, as the form does when it opens. "On top" rather than "open": with no chat selected the center stops drawing its page stack once its last page has closed, so a closed page can stay in its list. |
|
||||
| `showFullProfileDescription` "Read more" (R4) | replace if `PROFILE_DESCRIPTION` is on top | Also opened from info pages in the right pane. The text depends on which link was clicked. |
|
||||
| `showMarkdownFile` (R6) | desktop: replace if `MARKDOWN_FILE` is on top, on the main thread | The opener can be a support chat in the right pane, which must stay. Another file must show its own text. The file is read on a background thread, so the close and show run on the main thread, where the pane reads its stack. On Android the viewer covers its file once it has loaded; taps during the load are the async case under Not fixed. |
|
||||
| `showItemDetails` message "Info" (R3) | when a secondary chat is open, replace if `CHAT_ITEM_INFO` is on top | Info already closes the right pane first unless a secondary chat is open there. In that case it was pushed over the support or reports chat on every click. The first Info over the support chat still opens over it, and Back still returns to it. The job now runs on the chat's UI scope with only the load on a background thread, so the close and show run on the main thread, as `showReports` does. |
|
||||
|
||||
"Replace" closes and shows in one main-thread event, so the new page is drawn in the same place as the old one, from a lambda of the same call site, and Compose would keep the old page's state. That includes `AppBarTitle`'s `LaunchedEffect(title)`, so when the title is unchanged it would not reach the new page's app bar. Each replaced page, and each #6270 row, wraps its content in `key(this)`, the `ModalData` of that show, so it starts fresh.
|
||||
|
||||
The "on top" checks use a new `isTopModalOpen`, which looks at the visible top modal (the same index as `isLastModal`). A modal closed with animation stays in `modalViews` until its slide-out ends, and `isLastModalOpen` still counts it: "replace" during those 250 ms would then call `closeModal()` on a page already closing, and `closeModal()` would close the page under it. `isLastModalOpen` and its callers are unchanged.
|
||||
|
||||
### Not fixed
|
||||
|
||||
- **R7, What's New "Create your address".** The address page opened from What's New in the center pushes its own pages (Address settings, SimpleX name, 1-time link, Learn more) to the left pane, beside it. Opening it in the left pane instead, where they would cover it, makes it disposed while covered: on Back it is composed again, its `autoCreateAddress` effect runs again (re-creating an address the user just deleted) and its "share with contacts" state resets. The Settings → What's New path and Android already have that flaw. Fixing it needs `UserAddressView` to keep that state across disposal, which a follow-up PR (#7756) does.
|
||||
- Async openers that load data before showing a page, e.g. the reports and support bars, the add-members, channel-members and relays rows in group info, and on Android a `.md` file tapped twice while it loads. Two clicks within the load time can still push two pages. This is a different mechanism from the slide-in. It reproduces on master (reports bar, "Invite", "Invite members", "Member reports", "Chats with members" rows) and is fixed in a follow-up PR (#7757).
|
||||
- On desktop, two different `.md` files clicked before either has loaded: the one that finishes loading last is shown, as on master; Back now returns to the chat instead of the other file.
|
||||
- `OpenWhenReceivedEffect`: a file opened when its download finishes is not a repeated click. An image opens a second fullscreen viewer over an open one, as before. On desktop a `.md` file opened this way now replaces an open `.md` viewer, so Back returns to the chat instead of the other file.
|
||||
- Keyboard and accessibility activation are not pointer input and are not blocked during a slide.
|
||||
- The `BADGES` check (`hasModalOpen`) also counts a page that is sliding out, so on desktop a click within 250 ms of closing that page, if it slid out, does nothing; a click after the slide-out works.
|
||||
- A different page opened from the same view stacks over the first one, for example bio "Read more" over contact info, and Back returns to the first page. That is normal navigation. Of the buttons this change touches, only #6270's two rows, "Incognito" and "Learn more", replace each other.
|
||||
|
||||
## Effect
|
||||
|
||||
- Desktop: each fixed button opens its page once. A repeated click keeps the open page or replaces it with what was clicked. The on-top check doesn't depend on which button opened the page, so a bio, `.md` viewer or Info opened from one place is also replaced by one opened from another (for example a member's bio by a business chat banner's "Read more"), and Back then returns to the page under it. Message Info that replaces Info with nothing else on the right also starts fresh now: master kept the previous page's selected tab and, when both messages had the same title, its app bar showed none.
|
||||
- Both platforms: while a modal slides open, the page it covers in its pane, or the view under a pane's first modal, takes no new pointer input (clicks, drags, wheel scroll, hover) for 250 ms. On desktop the first page of the center and right panes appears without a slide, so it has no such window.
|
||||
- Both platforms: a keyed page (bio, `.md` viewer, Info, #6270's pages) opened again while the previous one is still sliding out after Back now starts fresh; master reused the closing page's remembered state (for example Info's selected tab), and its app bar showed no title when the title was unchanged.
|
||||
- Android: the slide window also covers modals the app opens itself (What's New at start), and a double-tapped toggle (chat info, the profile picker's incognito info, and the reports and support-chats bars when the second tap comes after their page is pushed) now stays open; a second tap during the reports or support-chat load still opens a second page (see Not fixed). Apart from these and the fresh state above, the per-site checks act only on desktop, where the button stays visible.
|
||||
|
||||
## Verification
|
||||
|
||||
Desktop: the same steps on the master prebuild and this branch, before and after, plus the areas the change touches (info and support chats in the right pane, profiles, badges, markdown viewer, Settings navigation and Back). Android (Waydroid): double tap on list rows, Back during a transition, and the touched flows.
|
||||
Reference in new issue
Block a user