diff --git a/storybook/pages/PrimaryNavSidebarPage.qml b/storybook/pages/PrimaryNavSidebarPage.qml index 48858f6d09..4fc12717c1 100644 --- a/storybook/pages/PrimaryNavSidebarPage.qml +++ b/storybook/pages/PrimaryNavSidebarPage.qml @@ -248,7 +248,6 @@ SplitView { } thirdpartyServicesEnabled: ctrlThirdPartyServices.checked - profileSectionHasNotification: ctrlSettingsHasNotification.checked onItemActivated: function (sectionType, sectionId) { logs.logEvent("onItemActivated", ["sectionType", "sectionId"], arguments) diff --git a/storybook/qmlTests/tests/tst_PrimaryNavSidebar.qml b/storybook/qmlTests/tests/tst_PrimaryNavSidebar.qml index 5f3fb614c1..8351ff966f 100644 --- a/storybook/qmlTests/tests/tst_PrimaryNavSidebar.qml +++ b/storybook/qmlTests/tests/tst_PrimaryNavSidebar.qml @@ -57,7 +57,6 @@ Item { return ["👨🏻‍🍼", "🏃🏿‍♂️", "🌇", "🤶🏿", "🏮"] } - profileSectionHasNotification: false thirdpartyServicesEnabled: true acVisible: false @@ -193,10 +192,8 @@ Item { verify(!!walletBtn) tryCompare(walletBtn, "visible", true) - // Check for Settings button - const settingsBtn = findChild(controlUnderTest, "Settings-navbar") - verify(!!settingsBtn) - tryCompare(settingsBtn, "visible", true) + // Settings is not a nav item anymore, it lives in the profile menu + verify(!findChild(controlUnderTest, "Settings-navbar")) } function test_section_button_click() { @@ -217,16 +214,9 @@ Item { verify(!!walletBtn) tryCompare(walletBtn, "checked", true) - // verify the Settings button is not checked - const settingsBtn = findChild(controlUnderTest, "Settings-navbar") - verify(!!settingsBtn) - tryCompare(settingsBtn, "checked", false) - // simulate changing the active section from outside (via mock model update) sidebarAdaptor.sectionsModel.setActiveSection("id3") // "id" of Constants.appSection.profile - // verify that Settings is active, Wallet is not - tryCompare(settingsBtn, "checked", true) tryCompare(walletBtn, "checked", false) } @@ -297,17 +287,6 @@ Item { verify(true) // Basic validation } - function test_profile_section_notification() { - controlUnderTest.profileSectionHasNotification = true - - const settingsBtn = findChild(controlUnderTest, "Settings-navbar") - verify(!!settingsBtn) - - // Settings button should show notification when profileSectionHasNotification is true - tryCompare(settingsBtn, "showBadge", true) - tryCompare(settingsBtn, "badgeVisible", true) - } - function test_community_buttons_have_object_name() { // Look for community buttons with specific objectName const communityBtn = findChild(controlUnderTest, "CommunityNavBarButton_Status.app") diff --git a/test/e2e/gui/components/online_identifier.py b/test/e2e/gui/components/online_identifier.py index 37ce6f29d4..13f382dff7 100644 --- a/test/e2e/gui/components/online_identifier.py +++ b/test/e2e/gui/components/online_identifier.py @@ -18,6 +18,7 @@ class OnlineIdentifier(QObject): self.automatic_button = Button(names.userContextmenu_AutomaticButton) self.view_my_profile_button = Button(names.userContextMenu_ViewMyProfileAction) self.copy_link_to_profile_button = QObject(names.userContextMenu_CopyLinkToProfile) + self.settings_button = Button(names.userContextMenu_SettingsAction) self.user_name_text_label = TextLabel(names.userStatusDisplayName) @property diff --git a/test/e2e/gui/main_window.py b/test/e2e/gui/main_window.py index c90ae61594..d9241f27c5 100644 --- a/test/e2e/gui/main_window.py +++ b/test/e2e/gui/main_window.py @@ -45,7 +45,6 @@ class MainLeftPanel(QObject): self.messages_button = Button(names.chatButton) self.communities_portal_button = Button(names.communitiesPortalButton) self.community_template_button = Button(names.statusCommunityMainNavBarListView_CommunityNavBarButton) - self.settings_button = Button(names.settingsGearButton) self.wallet_button = Button(names.mainWalletButton) self.activity_center_button = Button(names.activityCenterButton) self.market_button = Button(names.marketButton) @@ -91,10 +90,10 @@ class MainLeftPanel(QObject): def open_messages_screen(self): return self.messages_button - @allure.step('Click Gear button and open Settings screen') - @open_with_retries(SettingsScreen, attempts=3, delay=3.0) - def open_settings(self): - return self.settings_button + @allure.step('Open Settings screen from the profile context menu') + def open_settings(self) -> SettingsScreen: + self.open_online_identifier().settings_button.click() + return SettingsScreen().wait_until_appears() @allure.step('Click Activity center button and open Activity center panel') @open_with_retries(ActivityCenter) diff --git a/test/e2e/gui/objects_map/names.py b/test/e2e/gui/objects_map/names.py index 069daaf215..aba103abcb 100644 --- a/test/e2e/gui/objects_map/names.py +++ b/test/e2e/gui/objects_map/names.py @@ -59,12 +59,11 @@ chatButton = {"container": primaryNavSidebar, "objectName": "Messages-navbar", " "visible": True} marketButton = {"container": primaryNavSidebar, "objectName": "Market-navbar", "type": "RegularSectionButton", "visible": True} -# Second half of left main panel: communities button, settings button +# Second half of left main panel: communities button communitiesSettingsNavBarList = {"container": statusDesktop_mainWindow, "objectName": "statusMainNavBarListView", "type": "ListView", "visible": True} communitiesPortalButton = {"container": primaryNavSidebar, "objectName": "Communities Portal-navbar", "type": "PrimaryNavSidebarButton", "visible": True} -settingsGearButton = {"container": primaryNavSidebar, "objectName": "Settings-navbar", "type": "BottomSectionButton", "visible": True} activityCenterButton = {"container": statusDesktop_mainWindow, "objectName": "Activity Center-navbar", "type": "PrimaryNavSidebarButton", "visible": True} # Online identifier @@ -177,6 +176,9 @@ userContextMenu_ViewMyProfileAction = {"container": statusDesktop_mainWindow_ove userContextMenu_CopyLinkToProfile = {"container": statusDesktop_mainWindow_overlay, "objectName": "userStatusCopyLinkAction", "type": "StatusMenuItem", "visible": True} +userContextMenu_SettingsAction = {"container": statusDesktop_mainWindow_overlay, + "objectName": "userStatusSettingsAction", "type": "StatusMenuItem", + "visible": True} userStatusDisplayName = {"container": onlineIdentifier, "objectName": "userStatusDisplayName", "type": "StatusBaseText", "visible": True} diff --git a/test/e2e_appium/core/device_context.py b/test/e2e_appium/core/device_context.py index fc827901b6..73ed624e42 100644 --- a/test/e2e_appium/core/device_context.py +++ b/test/e2e_appium/core/device_context.py @@ -203,7 +203,7 @@ class DeviceContext: max_taps = 5 for tap_attempt in range(1, max_taps + 1): if not app.is_element_visible( - app_locators.LEFT_NAV_SETTINGS, timeout=1 + app_locators.PROFILE_NAV_BUTTON, timeout=1 ): self.logger.info( "Drawer closed after %d tap(s)", tap_attempt - 1 diff --git a/test/e2e_appium/locators/app_locators.py b/test/e2e_appium/locators/app_locators.py index cca706aa5b..d3c665a83c 100644 --- a/test/e2e_appium/locators/app_locators.py +++ b/test/e2e_appium/locators/app_locators.py @@ -16,9 +16,6 @@ class AppLocators(BaseLocators): LEFT_NAV_COMMUNITIES = BaseLocators.xpath( "//*[contains(@resource-id, 'Communities Portal-navbar')]" ) - LEFT_NAV_SETTINGS = BaseLocators.xpath( - "//*[contains(@resource-id, 'Settings-navbar')]" - ) # Home dock (visible only on Home) HOME_DOCK_CONTAINER = BaseLocators.xpath( @@ -45,6 +42,9 @@ class AppLocators(BaseLocators): SHARE_PROFILE_ACTION = BaseLocators.xpath( "//*[contains(@resource-id,'userStatusShareProfileAction')]" ) + SETTINGS_ACTION = BaseLocators.xpath( + "//*[contains(@resource-id,'userStatusSettingsAction')]" + ) PROFILE_MENU_CONTAINER = BaseLocators.xpath( "//*[contains(@resource-id,'ProfileMenu')]" ) diff --git a/test/e2e_appium/pages/app.py b/test/e2e_appium/pages/app.py index 460c889234..52614947af 100644 --- a/test/e2e_appium/pages/app.py +++ b/test/e2e_appium/pages/app.py @@ -24,7 +24,6 @@ class App(BasePage): "market": self.locators.LEFT_NAV_MARKET, "messaging": self.locators.LEFT_NAV_MESSAGES, "communities": self.locators.LEFT_NAV_COMMUNITIES, - "settings": self.locators.LEFT_NAV_SETTINGS, } for name, locator in mapping.items(): el = self.find_element_safe(locator, timeout=1) @@ -149,7 +148,7 @@ class App(BasePage): the app's QML picks layout based on width, so wide tablets in portrait still get the always-visible side-nav. """ - if self.is_element_visible(self.locators.LEFT_NAV_SETTINGS, timeout=2): + if self.is_element_visible(self.locators.PROFILE_NAV_BUTTON, timeout=2): return True from utils.screen_identity import dismiss_introduce_yourself @@ -164,11 +163,11 @@ class App(BasePage): return True if self.is_element_visible(self.locators.LEFT_NAV_ANY, timeout=1): - return self.is_element_visible(self.locators.LEFT_NAV_SETTINGS, timeout=5) + return self.is_element_visible(self.locators.PROFILE_NAV_BUTTON, timeout=5) # Phase 1: unwind deep navigation stack via back button for _ in range(5): - if self.is_element_visible(self.locators.LEFT_NAV_SETTINGS, timeout=1): + if self.is_element_visible(self.locators.PROFILE_NAV_BUTTON, timeout=1): return True if not self.is_element_visible( self.locators.TOOLBAR_BACK_BUTTON, timeout=1 @@ -176,7 +175,7 @@ class App(BasePage): break self.safe_click(self.locators.TOOLBAR_BACK_BUTTON, timeout=2) - if self.is_element_visible(self.locators.LEFT_NAV_SETTINGS, timeout=1): + if self.is_element_visible(self.locators.PROFILE_NAV_BUTTON, timeout=1): return True # Phase 2: drag the drawer handle to open the nav drawer @@ -185,7 +184,7 @@ class App(BasePage): break self.logger.debug("Nav drawer open attempt %d did not reveal nav", attempt + 1) - return self.is_element_visible(self.locators.LEFT_NAV_SETTINGS, timeout=5) + return self.is_element_visible(self.locators.PROFILE_NAV_BUTTON, timeout=5) # Locator for the drawer swipe-indicator handle visible in portrait mode. NAV_DRAWER_HANDLE = ( @@ -269,16 +268,36 @@ class App(BasePage): return False def click_settings_button(self) -> bool: - self.logger.info("Clicking Settings button") - if self.active_section() == "settings": + self.logger.info("Opening Settings from the profile menu") + from utils.screen_identity import SCREEN_ANCHORS + if self.is_element_visible(SCREEN_ANCHORS["settings"], timeout=1): self.logger.info("Already in Settings section — skipping nav") return True - from utils.screen_identity import SCREEN_ANCHORS - return self._click_drawer_nav_with_verify( - nav_locator=self.locators.LEFT_NAV_SETTINGS, - landmark_locator=SCREEN_ANCHORS["settings"], - nav_name="Settings", - ) + + for attempt in range(1, 4): + if not self._ensure_main_nav_visible(): + self.logger.warning( + "Nav not visible on Settings attempt %d", attempt + ) + continue + if not self.safe_click(self.locators.PROFILE_NAV_BUTTON, timeout=5): + self.logger.warning( + "Profile nav button tap failed on Settings attempt %d", attempt + ) + continue + if not self.safe_click(self.locators.SETTINGS_ACTION, timeout=5): + self.logger.warning( + "Settings menu item tap failed on attempt %d", attempt + ) + continue + if self.is_element_visible(SCREEN_ANCHORS["settings"], timeout=15): + return True + self.logger.warning( + "Settings screen anchor not visible after attempt %d", attempt + ) + + self.dump_page_source("settings_nav_failed") + return False def _click_drawer_nav_with_verify( self, diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index f2ba1cde55..d1c757bfca 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -1046,6 +1046,14 @@ Item { homePageLoader.item.focusSearch() } + function openSettingsRoot() { + profileLoader.settingsSubSubsection = -1 + profileLoader.settingsSubsection = appMain.isPortraitMode ? -1 : Constants.settingsSubsection.profile + appMain.rootStore.setActiveSectionBySectionType(Constants.appSection.profile) + if (profileLoader.item) + profileLoader.item.currentIndex = StatusSectionLayout.LeftPanel + } + function maybeDisplayIntroduceYourselfPopup() { if (!appMainLocalSettings.introduceYourselfPopupSeen && appMain.ownContactDetails?.displayName === "") { @@ -2623,17 +2631,6 @@ Item { communityPopupMenu: communityContextMenuComponent - profileSectionHasNotification: { - if (contactsModelAdaptor?.pendingReceivedRequestContacts?.ModelCount.count > 0) // pending contact request - return true - if (appMain.mainReady - && !appMain.privacyStore?.mnemonicBackedUp - && !appMain.profileStore?.userDeclinedBackupBanner) // seedphrase not backed up (removed) - return true - if (d.syncingBadgeCount > 0) // sync entries - return true - return false - } thirdpartyServicesEnabled: appMain.rootStore.thirdpartyServicesEnabled profileLoading: !appMain.mainReady @@ -2644,6 +2641,7 @@ Item { onSetCurrentUserStatusRequested: status => appMain.rootStore.setCurrentUserStatus(status) onViewProfileRequested: pubKey => Global.openProfilePopup(pubKey) onShareOwnProfileRequested: Global.shareProfileDialogRequested(appMain.ownContactDetails.publicKey) + onSettingsRequested: d.openSettingsRoot() onItemActivated: function(sectionType, sectionId) { // Ensure Activity Center Panel is closed when manual navigation done diff --git a/ui/app/mainui/adaptors/PrimaryNavSidebarAdaptor.qml b/ui/app/mainui/adaptors/PrimaryNavSidebarAdaptor.qml index 914cb7b751..d4798f2f54 100644 --- a/ui/app/mainui/adaptors/PrimaryNavSidebarAdaptor.qml +++ b/ui/app/mainui/adaptors/PrimaryNavSidebarAdaptor.qml @@ -83,10 +83,6 @@ SQUtils.QObject { roleName: "sectionType" value: Constants.appSection.qrCodeScanner } - ValueFilter { - roleName: "sectionType" - value: Constants.appSection.profile - } } } diff --git a/ui/app/mainui/panels/PrimaryNavSidebar.qml b/ui/app/mainui/panels/PrimaryNavSidebar.qml index deee8a7e85..b5871e49b1 100644 --- a/ui/app/mainui/panels/PrimaryNavSidebar.qml +++ b/ui/app/mainui/panels/PrimaryNavSidebar.qml @@ -52,7 +52,6 @@ Control { property Component communityPopupMenu // required property var model - required property bool profileSectionHasNotification required property bool thirdpartyServicesEnabled property bool profileLoading: false @@ -74,6 +73,7 @@ Control { signal activityCenterRequested(bool shouldShow) signal viewProfileRequested(string pubKey) signal shareOwnProfileRequested + signal settingsRequested signal setCurrentUserStatusRequested(int status) Component.onCompleted: d.snapToMode() @@ -293,6 +293,7 @@ Control { onSetCurrentUserStatusRequested: (status) => root.setCurrentUserStatusRequested(status) onViewProfileRequested: (pubKey) => root.viewProfileRequested(pubKey) onShareOwnProfileRequested: root.shareOwnProfileRequested() + onSettingsRequested: root.settingsRequested() } } } @@ -437,11 +438,7 @@ Control { } component BottomSectionButton: RegularSectionButton { - showBadge: { - if (model.sectionType === Constants.appSection.profile) - return root.profileSectionHasNotification - return model.hasNotification - } + showBadge: model.hasNotification } component SidebarListView: ListView { diff --git a/ui/imports/shared/controls/ProfileButton.qml b/ui/imports/shared/controls/ProfileButton.qml index d1869e1437..ba2644ff36 100644 --- a/ui/imports/shared/controls/ProfileButton.qml +++ b/ui/imports/shared/controls/ProfileButton.qml @@ -27,6 +27,7 @@ StatusIconTabButton { signal viewProfileRequested(string pubKey) signal shareOwnProfileRequested + signal settingsRequested signal setCurrentUserStatusRequested(int status) name: root.name @@ -100,6 +101,7 @@ StatusIconTabButton { onViewProfileRequested: root.viewProfileRequested(root.pubKey) onShareOwnProfileRequested: root.shareOwnProfileRequested() + onSettingsRequested: root.settingsRequested() onCopyLinkRequested: ClipboardUtils.setText(root.getLinkToProfileFn(root.pubKey)) onSetCurrentUserStatusRequested: (status) => root.setCurrentUserStatusRequested(status) } diff --git a/ui/imports/shared/popups/UserStatusContextMenu.qml b/ui/imports/shared/popups/UserStatusContextMenu.qml index ce98314315..96768dff29 100644 --- a/ui/imports/shared/popups/UserStatusContextMenu.qml +++ b/ui/imports/shared/popups/UserStatusContextMenu.qml @@ -29,6 +29,7 @@ StatusDropdown { signal viewProfileRequested signal copyLinkRequested signal shareOwnProfileRequested + signal settingsRequested signal setCurrentUserStatusRequested(int status) implicitWidth: 400 @@ -142,6 +143,18 @@ StatusDropdown { } } + ActionWrapper { + action: StatusAction { + objectName: "userStatusSettingsAction" + text: qsTr("Settings") + icon.name: "settings" + onTriggered: { + root.settingsRequested() + root.close() + } + } + } + StatusMenuSeparator { Layout.fillWidth: true }