Skip to content

Feature/ios ux#1952

Merged
Tlaster merged 3 commits intomasterfrom
feature/ios-ux
Apr 2, 2026
Merged

Feature/ios ux#1952
Tlaster merged 3 commits intomasterfrom
feature/ios-ux

Conversation

@Tlaster
Copy link
Copy Markdown
Contributor

@Tlaster Tlaster commented Apr 2, 2026

No description provided.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds UX improvements across iOS/Compose login and account management, including QR-based Nostr Connect onboarding and safer account removal flows.

Changes:

  • Implement QR-based Nostr Connect login session flow and expose QR state via NostrLoginState.
  • Update Compose login UI to use a single Nostr credential field and render a QR code + cancel flow.
  • Improve iOS/Android/Desktop account management UX (logout/delete confirmations, toolbar/tab picker behavior, sidebar restructuring).

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/login/NostrLoginPresenter.kt Adds QR login session state + start/cancel APIs for Nostr login presenter.
shared/src/commonMain/kotlin/dev/dimension/flare/data/network/nostr/NostrService.kt Introduces PendingQrLogin session, builds nostrconnect:// URI, and awaits remote signer approval.
iosApp/flare/UI/Screen/ProfileScreen.swift Refactors profile tab selection UI; adds toolbar-based picker visibility logic on compact devices.
iosApp/flare/UI/Screen/LocalHistoryScreen.swift Minor picker sizing tweak (fixedSize).
iosApp/flare/UI/Screen/AccountManagementScreen.swift Adds logout confirmation alert before removing an account locally.
iosApp/flare/UI/FlareRoot.swift Restructures iPad/sidebar tab layout using SecondaryTabsPresenter and static sidebar routes.
gradle/libs.versions.toml Adds qrose dependency version + library entry.
desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt Adds a confirmation dialog before deleting accounts in desktop settings.
desktopApp/src/main/composeResources/values/strings.xml Adds desktop confirmation string for account removal.
compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/login/ServiceSelectionScreenContent.kt Updates Nostr login UI: single credential input + QR code login UI (qrose).
compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/login/NostrInputPresenter.kt Simplifies Nostr input state to a single credential field and unified canLogin.
compose-ui/src/commonMain/composeResources/values/strings.xml Updates Nostr login copy and adds QR/cancel strings.
compose-ui/src/commonMain/composeResources/values-ja-rJP/strings.xml Japanese updates for new Nostr login copy + QR/cancel strings.
compose-ui/build.gradle.kts Adds qrose dependency to commonMain.
app/src/main/res/values/strings.xml Adds Android confirmation string for account removal.
app/src/main/java/dev/dimension/flare/ui/screen/settings/AccountsScreen.kt Adds confirmation dialog before deleting an account (including swipe-to-dismiss path).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +102 to +138
override fun startQrLogin() {
scope.launch {
runCatching {
loading = true
error = null
pendingQrLogin?.close()
val session = NostrService.beginQrLogin()
pendingQrLogin = session
qrConnectUri = session.connectUri
qrWaiting = true
loading = false
runCatching {
withTimeout(2.minutes) {
session.awaitAccount()
}
}.onSuccess { imported ->
if (pendingQrLogin === session) {
loading = true
pendingQrLogin = null
qrConnectUri = null
qrWaiting = false
loginWith(imported)
session.close()
loading = false
}
}.onFailure {
if (pendingQrLogin === session) {
error = it
pendingQrLogin = null
qrConnectUri = null
qrWaiting = false
session.close()
}
}
}.onFailure {
it.printStackTrace()
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In startQrLogin(), the outer runCatching { ... }.onFailure { printStackTrace() } can leave the UI in an inconsistent state (e.g., loading may remain true, qrWaiting/qrConnectUri may remain set, and no error is surfaced) when beginQrLogin() or other setup code throws. Consider handling this failure by setting error, resetting loading/qrWaiting/qrConnectUri/pendingQrLogin, and ensuring any partially-created session is closed (e.g., via try/finally).

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +125
loginWith(imported)
session.close()
loading = false
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inside the QR login success path, loginWith(imported) is called without a surrounding runCatching/try/finally. If loginWith throws (e.g., account persistence fails), loading will stay true and the session may never be closed. Wrap loginWith in error handling and ensure loading is reset and session.close() runs in a finally block.

Suggested change
loginWith(imported)
session.close()
loading = false
try {
loginWith(imported)
} catch (t: Throwable) {
error = t
t.printStackTrace()
} finally {
session.close()
loading = false
}

Copilot uses AI. Check for mistakes.
Comment on lines +307 to +323
val bunkerUri =
runCatching {
connect.bunkerUri().use { it.toString() }
}.getOrDefault(connectUri)
val signerRelay =
connect.relays().firstOrNull()?.use { it.toString() }
return ImportedAccount.RemoteSigner(
pubkeyHex = pubkeyHex,
npub = bech32PublicKey(pubkeyHex),
signerCredential =
NostrSignerCredential.Bunker(
uri = bunkerUri,
userPubkeyHex = pubkeyHex,
signerRelay = signerRelay,
secret = appSecret,
),
)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connect.relays() appears to return closeable relay objects (given the .use { ... }). Currently only the first relay is closed; any remaining relay objects from connect.relays() will leak. Consider mapping the relays to strings while closing each element (or otherwise ensuring all returned relay handles are closed).

Copilot uses AI. Check for mistakes.
ToolbarItem(placement: .primaryAction) {
profileTabPicker(tabs: tabs)
.pickerStyle(.menu)
.fixedSize()
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

profileTabPicker uses EmptyView() as the Picker label, but the same picker is rendered with .pickerStyle(.menu) in the compact toolbar. For menu-style pickers the label is what the user taps; an empty label can make the control effectively invisible/unusable and harms accessibility. Provide a visible label (e.g., current tab title or an icon) and/or an explicit accessibility label when using the menu style.

Suggested change
.fixedSize()
.fixedSize()
.overlay(
Image(systemName: "ellipsis.circle")
)
.accessibilityLabel(Text("Profile tabs"))

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6195eccd94

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 108 to +112
TabView(selection: $selectedTab) {
if horizontalSizeClass == .regular {
ForEach(tabs.primary, id: \.key) { data in
let badge = if data is NotificationTabItem || data is AllNotificationTabItem {
Int(notificationBadgePresenter.state.count)
} else {
0
}
Router { onNavigate in
data.view(onNavigate: onNavigate)
}
.tabItem {
Label {
TabTitle(title: data.metaData.title)
} icon: {
TabIcon(icon: data.metaData.icon, accountType: data.account, iconOnly: true)
}
}
.badge(badge)
.tag(data.key)
}
ForEach(tabs.secondary, id: \.key) { data in
Router { onNavigate in
data.view(onNavigate: onNavigate)
}
.tabItem {
Label {
TabTitle(title: data.metaData.title)
} icon: {
TabIcon(icon: data.metaData.icon, accountType: data.account, iconOnly: true)
}
}
.tag(data.key)
}
if let profileRoute = tabs.extraProfileRoute {
Router { onNavigate in
profileRoute.view(onNavigate: onNavigate)
}
.tabItem {
Label {
TabTitle(title: profileRoute.metaData.title)
} icon: {
TabIcon(icon: profileRoute.metaData.icon, accountType: profileRoute.account, iconOnly: true)
}
}
.tag(profileRoute.key)
ForEach(tabs.primary, id: \.key) { data in
let badge = if data is NotificationTabItem || data is AllNotificationTabItem {
Int(notificationBadgePresenter.state.count)
} else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore secondary/settings tabs in backport root

BackportFlareRoot now builds its TabView from tabs.primary only, while the previous regular-size branch that added tabs.secondary, extraProfileRoute, and the Settings tab was removed. Because FlareApp.swift routes all iOS versions below 18 to this view, iPad users on older iOS lose direct navigation to those sections entirely. Please reintroduce equivalent tabs/navigation for the backport path.

Useful? React with 👍 / 👎.

Comment on lines +136 to +137
}.onFailure {
it.printStackTrace()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear loading/error state when QR setup throws

If startQrLogin() fails before awaitAccount() handling starts (for example, beginQrLogin() throws), the outer onFailure only prints the stack trace and never resets loading or surfaces an error. In that failure mode the UI can remain stuck in a loading-disabled state without actionable feedback, so this path should set error and restore loading = false.

Useful? React with 👍 / 👎.

@Tlaster Tlaster merged commit f8f7b89 into master Apr 2, 2026
13 of 14 checks passed
@Tlaster Tlaster deleted the feature/ios-ux branch April 2, 2026 07:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants