Fix connectivity indicator and port signal bars from Android

The SignalView was never appearing because update_signal_from_pool was
defined but never called, leaving signal data at 0/0. Fix by having
RelayPool maintain its own SignalModel, updating it on connection
events, relay add/remove. Wire views directly to the pool signal via
NostrNetworkManager instead of the disconnected HomeModel.signal.

Replace the text-based indicator with signal strength bars matching
the Android app: 4 progressive-height bars colored red->yellow->green
based on the connected/total relay ratio.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
William Casarin
2026-05-18 11:00:00 -07:00
co-authored by Claude Opus 4.6
parent 07a7dbddc0
commit aae43cff3b
6 changed files with 77 additions and 25 deletions
+1 -1
View File
@@ -252,7 +252,7 @@ struct ContentView: View {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
HStack(alignment: .center) { HStack(alignment: .center) {
SignalView(state: damus_state!, signal: home.signal) SignalView(state: damus_state!, signal: damus_state!.nostrNetwork.signal)
// maybe expand this to other timelines in the future // maybe expand this to other timelines in the future
if selected_timeline == .search { if selected_timeline == .search {
@@ -247,6 +247,11 @@ class NostrNetworkManager {
self.pool.relays self.pool.relays
} }
@MainActor
var signal: SignalModel {
self.pool.signal
}
@MainActor @MainActor
var ourRelayDescriptors: [RelayPool.RelayDescriptor] { var ourRelayDescriptors: [RelayPool.RelayDescriptor] {
self.pool.our_descriptors self.pool.our_descriptors
+16
View File
@@ -51,6 +51,7 @@ class RelayPool {
var message_received_function: (((String, RelayDescriptor)) -> Void)? var message_received_function: (((String, RelayDescriptor)) -> Void)?
var message_sent_function: (((String, Relay)) -> Void)? var message_sent_function: (((String, Relay)) -> Void)?
var delegate: Delegate? var delegate: Delegate?
@MainActor
private(set) var signal: SignalModel = SignalModel() private(set) var signal: SignalModel = SignalModel()
/// Tracks active leases on ephemeral relays to prevent premature cleanup. /// Tracks active leases on ephemeral relays to prevent premature cleanup.
@@ -125,6 +126,18 @@ class RelayPool {
return relays.reduce(0) { n, r in n + (r.connection.isConnected ? 1 : 0) } return relays.reduce(0) { n, r in n + (r.connection.isConnected ? 1 : 0) }
} }
@MainActor
func update_signal() {
let connected = num_connected
let total = relays.count
if signal.signal != connected {
signal.signal = connected
}
if signal.max_signal != total {
signal.max_signal = total
}
}
func remove_handler(sub_id: String) { func remove_handler(sub_id: String) {
self.handlers = handlers.filter { self.handlers = handlers.filter {
if $0.sub_id != sub_id { if $0.sub_id != sub_id {
@@ -183,6 +196,7 @@ class RelayPool {
i += 1 i += 1
} }
update_signal()
} }
/// Acquires a lease on ephemeral relays to prevent them from being cleaned up /// Acquires a lease on ephemeral relays to prevent them from being cleaned up
@@ -269,6 +283,7 @@ class RelayPool {
@MainActor @MainActor
private func appendRelayToList(relay: Relay) { private func appendRelayToList(relay: Relay) {
self.relays.append(relay) self.relays.append(relay)
update_signal()
} }
/// Ensures the given relay URLs are connected, adding them as ephemeral relays if not already in the pool. /// Ensures the given relay URLs are connected, adding them as ephemeral relays if not already in the pool.
@@ -761,6 +776,7 @@ class RelayPool {
run_queue(relay_id) run_queue(relay_id)
await self.resubscribeAll(relayId: relay_id) await self.resubscribeAll(relayId: relay_id)
} }
await update_signal()
} }
// Handle auth // Handle auth
+53 -8
View File
@@ -11,24 +11,69 @@ struct SignalView: View {
let state: DamusState let state: DamusState
@ObservedObject var signal: SignalModel @ObservedObject var signal: SignalModel
static let num_bars = 4
static let bar_heights: [CGFloat] = [4, 7, 10, 13]
static let bar_width: CGFloat = 3
static let bar_spacing: CGFloat = 2
var ratio: Double {
guard signal.max_signal > 0 else { return 0 }
return Double(signal.signal) / Double(signal.max_signal)
}
var active_bars: Int {
if signal.signal == 0 { return 0 }
return max(1, min(Self.num_bars, Int(ceil(ratio * Double(Self.num_bars)))))
}
var active_color: Color {
if ratio < 0.5 {
let t = ratio * 2.0
return Color(
red: 1.0,
green: 0.4 + 0.4 * t,
blue: 0.4
)
} else {
let t = (ratio - 0.5) * 2.0
return Color(
red: 1.0 - 0.6 * t,
green: 0.8,
blue: 0.4
)
}
}
var inactive_color: Color {
Color.gray.opacity(0.3)
}
var body: some View { var body: some View {
Group { Group {
if signal.signal != signal.max_signal { if signal.max_signal > 0 {
NavigationLink(value: Route.RelayConfig) { NavigationLink(value: Route.RelayConfig) {
Text("\(signal.signal)/\(signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.") HStack(alignment: .bottom, spacing: Self.bar_spacing) {
.font(.callout) ForEach(0..<Self.num_bars, id: \.self) { i in
.foregroundColor(.gray) RoundedRectangle(cornerRadius: 1)
.fill(i < active_bars ? active_color : inactive_color)
.frame(width: Self.bar_width, height: Self.bar_heights[i])
}
}
} }
.frame(width:50,height:30) .frame(width: 30, height: 30)
.disabled(signal.signal == signal.max_signal) .accessibilityLabel(Text("\(signal.signal)/\(signal.max_signal) relays connected"))
} }
} }
} }
} }
struct SignalView_Previews: PreviewProvider { struct SignalView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
SignalView(state: test_damus_state, signal: SignalModel(signal: 5, max_signal: 10)) HStack(spacing: 20) {
SignalView(state: test_damus_state, signal: SignalModel(signal: 0, max_signal: 10))
SignalView(state: test_damus_state, signal: SignalModel(signal: 3, max_signal: 10))
SignalView(state: test_damus_state, signal: SignalModel(signal: 5, max_signal: 10))
SignalView(state: test_damus_state, signal: SignalModel(signal: 10, max_signal: 10))
}
} }
} }
@@ -77,8 +77,6 @@ class HomeModel: ContactsDelegate, ObservableObject {
@Published var loading: Bool = true @Published var loading: Bool = true
var signal = SignalModel()
var notifications = NotificationsModel() var notifications = NotificationsModel()
var notification_status = NotificationStatusModel() var notification_status = NotificationStatusModel()
var events: EventHolder = EventHolder() var events: EventHolder = EventHolder()
@@ -1037,18 +1035,6 @@ class HomeModel: ContactsDelegate, ObservableObject {
} }
func update_signal_from_pool(signal: SignalModel, pool: RelayPool) async {
let relayCount = await pool.relays.count
if signal.max_signal != relayCount {
signal.max_signal = relayCount
}
let numberOfConnectedRelays = await pool.num_connected
if signal.signal != numberOfConnectedRelays {
signal.signal = numberOfConnectedRelays
}
}
@MainActor @MainActor
func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) { func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) {
if !contacts.is_friend(ev.pubkey) { if !contacts.is_friend(ev.pubkey) {
@@ -82,7 +82,7 @@ struct PostingTimelineView: View {
Spacer() Spacer()
HStack(alignment: .center) { HStack(alignment: .center) {
SignalView(state: damus_state, signal: home.signal) SignalView(state: damus_state, signal: damus_state.nostrNetwork.signal)
if damus_state.settings.enable_favourites_feature { if damus_state.settings.enable_favourites_feature {
Image(systemName: "square.stack") Image(systemName: "square.stack")
.foregroundColor(DamusColors.purple) .foregroundColor(DamusColors.purple)