Files
bitchat/bitchat/Views/AppInfoView.swift
T
ef848857b7 Remove dead code found by full Periphery audit; add scan config + advisory CI (#1410)
Periphery 3.7.4 audit of both schemes (macOS + iOS, intersected so
platform-specific code is never touched), with test targets indexed and
the share extension built. 277 dead declarations removed or demoted:
dead forwarding wrappers (ChatViewModel+Nostr/+PrivateChat), removed-
feature remnants (autocomplete command suggestions, back-swipe tuning,
MediaSendError, GeohashParticipantTracker), unused Tor dormancy
bindings, assign-only properties, unused parameters (renamed to _), and
redundant public accessibility. 13 orphaned localization keys deleted
across all 29 locales (old pre-#1392 location-notes UI, app_info
warnings).

Two real tests were flagged as unused because they never ran: Swift
Testing methods missing @Test (NostrProtocolTests.
testAckRoundTripNIP44V2_Delivered, NotificationStreamAssemblerTests.
testAssemblesCompressedLargeFrame). Re-armed both; they pass.

Deliberately kept, now recorded in .periphery.baseline.json: iOS-only
code invisible to the CI macOS scan, C FFI signatures, keep-alive
NWPathMonitor reference, InboundEventKey.eventID (dedup semantics),
wifiBulk capability bit (reserved for Wi-Fi bulk work, used by
BitFoundation package tests), and the String secureClear cluster
(exercised by package tests).

New: .periphery.yml config and an advisory Dead Code CI job (mirrors
the SwiftLint precedent from #1361) that fails on findings not in the
committed baseline.

Verified: full macOS app suite, BitFoundation (119) and BitLogger (13)
package tests green; periphery scan --strict exits clean.

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 11:24:19 +02:00

411 lines
15 KiB
Swift

import SwiftUI
struct AppInfoView: View {
@Environment(\.dismiss) var dismiss
@ThemedPalette private var palette
@AppStorage(AppTheme.storageKey) private var appThemeRawValue = AppTheme.matrix.rawValue
/// Supplies the mesh topology map data. Nil (previews, missing wiring)
/// hides the topology row entirely.
var topologyProvider: (@MainActor () -> MeshTopologyDisplayModel)?
@State private var showTopology = false
@State private var liveVoiceEnabled = PTTSettings.liveVoiceEnabled
private var selectedTheme: AppTheme {
AppTheme(rawValue: appThemeRawValue) ?? .matrix
}
private var textColor: Color { palette.primary }
private var secondaryTextColor: Color { palette.secondary }
// MARK: - Constants
private enum Strings {
static let appName: LocalizedStringKey = "app_info.app_name"
static let tagline: LocalizedStringKey = "app_info.tagline"
static let appearanceTitle: LocalizedStringKey = "app_info.appearance.title"
enum Features {
static let title: LocalizedStringKey = "app_info.features.title"
static let offlineComm = AppInfoFeatureInfo(
icon: "wifi.slash",
title: "app_info.features.offline.title",
description: "app_info.features.offline.description"
)
static let encryption = AppInfoFeatureInfo(
icon: "lock.shield",
title: "app_info.features.encryption.title",
description: "app_info.features.encryption.description"
)
static let extendedRange = AppInfoFeatureInfo(
icon: "antenna.radiowaves.left.and.right",
title: "app_info.features.extended_range.title",
description: "app_info.features.extended_range.description"
)
static let mentions = AppInfoFeatureInfo(
icon: "at",
title: "app_info.features.mentions.title",
description: "app_info.features.mentions.description"
)
static let favorites = AppInfoFeatureInfo(
icon: "star.fill",
title: "app_info.features.favorites.title",
description: "app_info.features.favorites.description"
)
static let geohash = AppInfoFeatureInfo(
icon: "number",
title: "app_info.features.geohash.title",
description: "app_info.features.geohash.description"
)
}
enum Legend {
static let title: LocalizedStringKey = "app_info.legend.title"
/// Every glyph the peer lists and headers use, in one place —
/// nothing else in the app defines them.
static let items: [(icon: String, text: LocalizedStringKey)] = [
("antenna.radiowaves.left.and.right", "app_info.legend.mesh_connected"),
("point.3.filled.connected.trianglepath.dotted", "app_info.legend.mesh_relayed"),
("globe", "app_info.legend.nostr"),
("person", "app_info.legend.offline"),
("mappin.and.ellipse", "app_info.legend.location_nearby"),
("face.dashed", "app_info.legend.teleported"),
("lock.fill", "app_info.legend.encrypted"),
("lock.slash", "app_info.legend.encryption_failed"),
("checkmark.seal.fill", "app_info.legend.verified"),
("star.fill", "app_info.legend.favorite"),
("envelope.fill", "app_info.legend.unread"),
("nosign", "app_info.legend.blocked")
]
}
enum Voice {
static let title: LocalizedStringKey = "app_info.voice.title"
static let live = AppInfoFeatureInfo(
icon: "dot.radiowaves.left.and.right",
title: "app_info.voice.live.title",
description: "app_info.voice.live.description"
)
}
enum Network {
static let title: LocalizedStringKey = "app_info.network.title"
static let topology = AppInfoFeatureInfo(
icon: "point.3.connected.trianglepath.dotted",
title: "app_info.network.topology.title",
description: "app_info.network.topology.description"
)
}
enum Privacy {
static let title: LocalizedStringKey = "app_info.privacy.title"
static let noTracking = AppInfoFeatureInfo(
icon: "eye.slash",
title: "app_info.privacy.no_tracking.title",
description: "app_info.privacy.no_tracking.description"
)
static let ephemeral = AppInfoFeatureInfo(
icon: "shuffle",
title: "app_info.privacy.ephemeral.title",
description: "app_info.privacy.ephemeral.description"
)
static let panic = AppInfoFeatureInfo(
icon: "hand.raised.fill",
title: "app_info.privacy.panic.title",
description: "app_info.privacy.panic.description"
)
}
enum HowToUse {
static let title: LocalizedStringKey = "app_info.how_to_use.title"
/// The instruction strings flowed into one comma-separated
/// paragraph. The translations carry their legacy bullet-list
/// prefix ("• "), so it is stripped here.
static var paragraph: String {
[
String(localized: "app_info.how_to_use.set_nickname"),
String(localized: "app_info.how_to_use.change_channels"),
String(localized: "app_info.how_to_use.open_sidebar"),
String(localized: "app_info.how_to_use.start_dm"),
String(localized: "app_info.how_to_use.clear_chat"),
String(localized: "app_info.how_to_use.commands")
]
.map { $0.hasPrefix("• ") ? String($0.dropFirst(2)) : $0 }
.joined(separator: ", ")
}
}
}
var body: some View {
#if os(macOS)
VStack(spacing: 0) {
// Custom header for macOS
HStack {
Spacer()
Button("app_info.done") {
dismiss()
}
.buttonStyle(.plain)
.foregroundColor(textColor)
.padding()
}
.themedSurface(opacity: 0.95)
ScrollView {
infoContent
}
.themedSheetBackground()
}
.frame(width: 600, height: 700)
.sheet(isPresented: $showTopology) {
if let topologyProvider {
MeshTopologyView(provider: topologyProvider)
}
}
#else
NavigationView {
ScrollView {
infoContent
}
.themedSheetBackground()
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
SheetCloseButton { dismiss() }
.foregroundColor(textColor)
}
}
}
.sheet(isPresented: $showTopology) {
if let topologyProvider {
MeshTopologyView(provider: topologyProvider)
}
}
#endif
}
@ViewBuilder
private var infoContent: some View {
VStack(alignment: .leading, spacing: 24) {
// Header
VStack(alignment: .center, spacing: 8) {
Text(Strings.appName)
.bitchatFont(size: 32, weight: .bold)
.foregroundColor(textColor)
Text(Strings.tagline)
.bitchatFont(size: 16)
.foregroundColor(secondaryTextColor)
}
.frame(maxWidth: .infinity)
.padding(.vertical)
// How to Use
VStack(alignment: .leading, spacing: 16) {
SectionHeader(Strings.HowToUse.title)
Text(verbatim: Strings.HowToUse.paragraph)
.bitchatFont(size: 14)
.foregroundColor(textColor)
.fixedSize(horizontal: false, vertical: true)
}
// Appearance — single row: label left, theme chips right
HStack(spacing: 12) {
SectionHeader(Strings.appearanceTitle)
Spacer()
ForEach(AppTheme.allCases) { theme in
Button {
appThemeRawValue = theme.rawValue
} label: {
Text(theme.displayNameKey)
.bitchatFont(size: 13, weight: selectedTheme == theme ? .semibold : .regular)
.foregroundColor(selectedTheme == theme ? palette.accent : secondaryTextColor)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(selectedTheme == theme ? palette.accent.opacity(0.15) : Color.clear)
)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityAddTraits(selectedTheme == theme ? .isSelected : [])
}
}
// Voice
VStack(alignment: .leading, spacing: 16) {
SectionHeader(Strings.Voice.title)
HStack(spacing: 0) {
FeatureRow(info: Strings.Voice.live)
Toggle(Strings.Voice.live.title, isOn: $liveVoiceEnabled)
.labelsHidden()
.tint(palette.accent)
.onChange(of: liveVoiceEnabled) { newValue in
PTTSettings.liveVoiceEnabled = newValue
}
}
}
// Voice
VStack(alignment: .leading, spacing: 16) {
SectionHeader(Strings.Voice.title)
HStack(spacing: 0) {
FeatureRow(info: Strings.Voice.live)
Toggle(Strings.Voice.live.title, isOn: $liveVoiceEnabled)
.labelsHidden()
.tint(palette.accent)
.onChange(of: liveVoiceEnabled) { newValue in
PTTSettings.liveVoiceEnabled = newValue
}
}
}
// Network diagnostics
if topologyProvider != nil {
VStack(alignment: .leading, spacing: 16) {
SectionHeader(Strings.Network.title)
Button {
showTopology = true
} label: {
HStack(spacing: 0) {
FeatureRow(info: Strings.Network.topology)
Image(systemName: "chevron.right")
.font(.bitchatSystem(size: 12))
.foregroundColor(secondaryTextColor)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityHint(Text("app_info.network.topology.hint"))
}
}
// Features
VStack(alignment: .leading, spacing: 16) {
SectionHeader(Strings.Features.title)
FeatureRow(info: Strings.Features.offlineComm)
FeatureRow(info: Strings.Features.encryption)
FeatureRow(info: Strings.Features.extendedRange)
FeatureRow(info: Strings.Features.favorites)
FeatureRow(info: Strings.Features.geohash)
FeatureRow(info: Strings.Features.mentions)
}
// Privacy
VStack(alignment: .leading, spacing: 16) {
SectionHeader(Strings.Privacy.title)
FeatureRow(info: Strings.Privacy.noTracking)
FeatureRow(info: Strings.Privacy.ephemeral)
FeatureRow(info: Strings.Privacy.panic)
}
// Symbols legend
VStack(alignment: .leading, spacing: 10) {
SectionHeader(Strings.Legend.title)
ForEach(Strings.Legend.items, id: \.icon) { item in
HStack(alignment: .top, spacing: 12) {
Image(systemName: item.icon)
.font(.bitchatSystem(size: 14))
.foregroundColor(textColor)
.frame(width: 30)
Text(item.text)
.bitchatFont(size: 13)
.foregroundColor(secondaryTextColor)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.accessibilityElement(children: .combine)
}
}
}
.padding()
}
}
struct AppInfoFeatureInfo {
let icon: String
let title: LocalizedStringKey
let description: LocalizedStringKey
}
struct SectionHeader: View {
let title: LocalizedStringKey
@ThemedPalette private var palette
private var textColor: Color { palette.primary }
init(_ title: LocalizedStringKey) {
self.title = title
}
var body: some View {
Text(title)
.bitchatFont(size: 16, weight: .bold)
.foregroundColor(textColor)
.padding(.top, 8)
}
}
struct FeatureRow: View {
let info: AppInfoFeatureInfo
@ThemedPalette private var palette
private var textColor: Color { palette.primary }
private var secondaryTextColor: Color { palette.secondary }
var body: some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: info.icon)
.font(.bitchatSystem(size: 20))
.foregroundColor(textColor)
.frame(width: 30)
VStack(alignment: .leading, spacing: 4) {
Text(info.title)
.bitchatFont(size: 14, weight: .semibold)
.foregroundColor(textColor)
Text(info.description)
.bitchatFont(size: 12)
.foregroundColor(secondaryTextColor)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
}
}
}
#Preview("Default") {
AppInfoView()
}
#Preview("Dynamic Type XXL") {
AppInfoView()
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
}
#Preview("Dynamic Type XS") {
AppInfoView()
.environment(\.sizeCategory, .extraSmall)
}