Merge e422cfd0a8930b523a41269d9265a095f8bb5d5f into 1f59e814f90c3f489f48d68262cb1bf640bf6181

This commit is contained in:
Taksh Kothari 2026-08-06 01:44:39 +00:00 committed by GitHub
commit c163dc4aa2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 1561 additions and 0 deletions

View File

@ -0,0 +1,74 @@
{
"images": [
{
"filename": "icon_1024x1024.png",
"idiom": "universal",
"platform": "ios",
"size": "1024x1024"
},
{
"filename": "icon_16x16.png",
"idiom": "mac",
"scale": "1x",
"size": "16x16"
},
{
"filename": "icon_16x16@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "16x16"
},
{
"filename": "icon_32x32.png",
"idiom": "mac",
"scale": "1x",
"size": "32x32"
},
{
"filename": "icon_32x32@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "32x32"
},
{
"filename": "icon_128x128.png",
"idiom": "mac",
"scale": "1x",
"size": "128x128"
},
{
"filename": "icon_128x128@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "128x128"
},
{
"filename": "icon_256x256.png",
"idiom": "mac",
"scale": "1x",
"size": "256x256"
},
{
"filename": "icon_256x256@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "256x256"
},
{
"filename": "icon_512x512.png",
"idiom": "mac",
"scale": "1x",
"size": "512x512"
},
{
"filename": "icon_512x512@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "512x512"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,74 @@
{
"images": [
{
"filename": "icon_1024x1024.png",
"idiom": "universal",
"platform": "ios",
"size": "1024x1024"
},
{
"filename": "icon_16x16.png",
"idiom": "mac",
"scale": "1x",
"size": "16x16"
},
{
"filename": "icon_16x16@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "16x16"
},
{
"filename": "icon_32x32.png",
"idiom": "mac",
"scale": "1x",
"size": "32x32"
},
{
"filename": "icon_32x32@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "32x32"
},
{
"filename": "icon_128x128.png",
"idiom": "mac",
"scale": "1x",
"size": "128x128"
},
{
"filename": "icon_128x128@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "128x128"
},
{
"filename": "icon_256x256.png",
"idiom": "mac",
"scale": "1x",
"size": "256x256"
},
{
"filename": "icon_256x256@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "256x256"
},
{
"filename": "icon_512x512.png",
"idiom": "mac",
"scale": "1x",
"size": "512x512"
},
{
"filename": "icon_512x512@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "512x512"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,74 @@
{
"images": [
{
"filename": "icon_1024x1024.png",
"idiom": "universal",
"platform": "ios",
"size": "1024x1024"
},
{
"filename": "icon_16x16.png",
"idiom": "mac",
"scale": "1x",
"size": "16x16"
},
{
"filename": "icon_16x16@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "16x16"
},
{
"filename": "icon_32x32.png",
"idiom": "mac",
"scale": "1x",
"size": "32x32"
},
{
"filename": "icon_32x32@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "32x32"
},
{
"filename": "icon_128x128.png",
"idiom": "mac",
"scale": "1x",
"size": "128x128"
},
{
"filename": "icon_128x128@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "128x128"
},
{
"filename": "icon_256x256.png",
"idiom": "mac",
"scale": "1x",
"size": "256x256"
},
{
"filename": "icon_256x256@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "256x256"
},
{
"filename": "icon_512x512.png",
"idiom": "mac",
"scale": "1x",
"size": "512x512"
},
{
"filename": "icon_512x512@2x.png",
"idiom": "mac",
"scale": "2x",
"size": "512x512"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -45,6 +45,10 @@ struct BitchatApp: App {
.onAppear {
appDelegate.runtime = runtime
runtime.start()
// No-op on macOS / devices without alternate-icon support;
// kept outside `#if os(iOS)` so the macOS Periphery scan
// sees the launch-time apply path.
AlternateAppIconSettings.applyStoredPreference()
}
.onOpenURL { url in
runtime.handleOpenURL(url)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,121 @@
//
// AlternateAppIconSettings.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
#if os(iOS)
import UIKit
#endif
import BitLogger
/// Low-profile alternate home-screen icons (#1447). Neutral / notes / quiet
/// variants live as App Icon sets in the asset catalog and are enabled via
/// `ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS`.
enum AlternateAppIconSettings {
static let storageKey = "appearance.alternateAppIcon"
/// Catalog names. `nil` means the primary AppIcon.
enum Icon: String, CaseIterable, Identifiable {
case primary
case neutral = "AppIconNeutral"
case notes = "AppIconNotes"
case quiet = "AppIconQuiet"
var id: String { rawValue }
/// Name passed to `UIApplication.setAlternateIconName`. Primary is nil.
var systemName: String? {
switch self {
case .primary: return nil
case .neutral, .notes, .quiet: return rawValue
}
}
// periphery:ignore - read from the iOS App Info icon picker; the macOS
// Periphery scheme cannot see that use site (#1447).
var title: String {
switch self {
case .primary:
return String(
localized: "app_info.settings.icon.primary",
defaultValue: "bitchat",
comment: "Label for the default bitchat home-screen icon"
)
case .neutral:
return String(
localized: "app_info.settings.icon.neutral",
defaultValue: "neutral",
comment: "Label for the grayscale low-profile alternate app icon"
)
case .notes:
return String(
localized: "app_info.settings.icon.notes",
defaultValue: "notes",
comment: "Label for the blue-gray notes-like alternate app icon"
)
case .quiet:
return String(
localized: "app_info.settings.icon.quiet",
defaultValue: "quiet",
comment: "Label for the dark monochrome alternate app icon"
)
}
}
}
static func selected(in defaults: UserDefaults) -> Icon {
let raw = defaults.string(forKey: storageKey) ?? Icon.primary.rawValue
return Icon(rawValue: raw) ?? .primary
}
static func setSelected(_ icon: Icon, in defaults: UserDefaults, applySystem: Bool = true) {
if icon == .primary {
defaults.removeObject(forKey: storageKey)
} else {
defaults.set(icon.rawValue, forKey: storageKey)
}
if applySystem {
apply(icon)
}
}
/// Apply the stored preference to the system (iOS only). Safe no-op elsewhere.
static func applyStoredPreference() {
apply(selected(in: .standard))
}
/// Panic wipe clears the *stored* preference only do not call `apply`.
/// `setAlternateIconName` pops a system alert that names "bitchat", which
/// would un-disguise the phone at the worst moment. The home-screen glyph
/// stays until the next intentional picker change or relaunch policy.
static func reset(in defaults: UserDefaults = .standard) {
defaults.removeObject(forKey: storageKey)
}
private static func apply(_ icon: Icon) {
#if os(iOS)
// Unit tests and early launch may not have a ready UIApplication.
guard Thread.isMainThread else {
DispatchQueue.main.async { apply(icon) }
return
}
guard UIApplication.shared.supportsAlternateIcons else { return }
let target = icon.systemName
// Avoid a no-op system prompt when already on the desired icon.
let current = UIApplication.shared.alternateIconName
guard current != target else { return }
UIApplication.shared.setAlternateIconName(target) { error in
if let error {
SecureLogger.warning(
"alternate icon change failed: \(error.localizedDescription)",
category: .session
)
}
}
#endif
}
}

View File

@ -1637,6 +1637,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
MeshSightingsTracker.shared.clear()
MeshEchoSettings.reset()
NotificationPrivacySettings.reset()
AlternateAppIconSettings.reset()
// A hand-added relay names an operator someone chose to route through,
// which is the kind of trace a wipe should not leave behind.
NostrRelaySettings.reset()

View File

@ -34,6 +34,9 @@ struct AppInfoView: View {
/// The override changed this session; localization resolves at process
/// start, so surface the restart hint.
@State private var showLanguageRestartNote = false
#if os(iOS)
@State private var selectedAppIcon = AlternateAppIconSettings.selected(in: .standard)
#endif
private enum Pane: String {
case settings
@ -61,6 +64,9 @@ struct AppInfoView: View {
static let tabSettings = String(localized: "app_info.tab.settings", defaultValue: "settings", comment: "Segmented control label for the settings pane of the app info sheet")
static let tabInfo = String(localized: "app_info.tab.info", defaultValue: "info", comment: "Segmented control label for the info pane of the app info sheet")
static let iconTitle = String(localized: "app_info.settings.icon.title", defaultValue: "HOME SCREEN ICON", comment: "Section header (uppercase) for alternate app icon picker") // periphery:ignore - iOS App Info settings UI
static let iconSubtitle = String(localized: "app_info.settings.icon.subtitle", defaultValue: "optional low-profile icons. the home-screen label still reads “bitchat”, and the real name still shows in settings and search — this only changes the glyph.", comment: "Caption under the alternate app icon picker explaining the limits of disguise") // periphery:ignore - iOS App Info settings UI
static let connectivityTitle = String(localized: "app_info.settings.connectivity.title", defaultValue: "CONNECTIVITY", comment: "Section header (uppercase) for the connectivity toggles: mesh bridge, internet gateway, tor routing")
static let languageTitle = String(localized: "app_info.settings.language.title", defaultValue: "LANGUAGE", comment: "Section header (uppercase) for the app language picker in settings")
@ -357,6 +363,51 @@ struct AppInfoView: View {
}
}
#if os(iOS)
// Low-profile home-screen icons for the opsec / disguise path (#1447).
// macOS has no alternate-icon API equivalent, so this stays iOS-only.
VStack(alignment: .leading, spacing: 12) {
SectionHeader(verbatim: Strings.Settings.iconTitle)
settingsCard {
Text(verbatim: Strings.Settings.iconSubtitle)
.bitchatFont(size: 11)
.foregroundColor(secondaryTextColor)
HStack(spacing: 8) {
ForEach(AlternateAppIconSettings.Icon.allCases) { icon in
Button {
selectedAppIcon = icon
AlternateAppIconSettings.setSelected(icon, in: .standard, applySystem: true)
} label: {
Text(verbatim: icon.title)
.bitchatFont(
size: 12,
weight: selectedAppIcon == icon ? .semibold : .regular
)
.foregroundColor(
selectedAppIcon == icon ? palette.accent : secondaryTextColor
)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(
selectedAppIcon == icon
? palette.accent.opacity(0.15)
: Color.clear
)
)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityAddTraits(selectedAppIcon == icon ? .isSelected : [])
}
}
}
}
#endif
// Language an in-app override so the UI language can differ
// from the device language (takes effect on next launch).
VStack(alignment: .leading, spacing: 12) {

View File

@ -0,0 +1,52 @@
import Foundation
import Testing
@testable import bitchat
struct AlternateAppIconSettingsTests {
private func makeDefaults() -> (suite: String, defaults: UserDefaults) {
let suite = "bitchat.tests.alticon.\(UUID().uuidString)"
return (suite, UserDefaults(suiteName: suite)!)
}
private func cleanup(_ suite: String) {
UserDefaults().removePersistentDomain(forName: suite)
}
@Test func primarySystemNameIsNil() {
#expect(AlternateAppIconSettings.Icon.primary.systemName == nil)
}
@Test func alternateSystemNamesMatchCatalog() {
#expect(AlternateAppIconSettings.Icon.neutral.systemName == "AppIconNeutral")
#expect(AlternateAppIconSettings.Icon.notes.systemName == "AppIconNotes")
#expect(AlternateAppIconSettings.Icon.quiet.systemName == "AppIconQuiet")
}
@Test func allCasesCoverDisguiseSet() {
let ids = Set(AlternateAppIconSettings.Icon.allCases.map(\.id))
#expect(ids == ["primary", "AppIconNeutral", "AppIconNotes", "AppIconQuiet"])
}
@Test func selectedPersistsRawValueWithoutTouchingSystem() {
let (suite, defaults) = makeDefaults()
defer { cleanup(suite) }
#expect(AlternateAppIconSettings.selected(in: defaults) == .primary)
AlternateAppIconSettings.setSelected(.notes, in: defaults, applySystem: false)
#expect(defaults.string(forKey: AlternateAppIconSettings.storageKey) == "AppIconNotes")
#expect(AlternateAppIconSettings.selected(in: defaults) == .notes)
AlternateAppIconSettings.setSelected(.primary, in: defaults, applySystem: false)
#expect(defaults.string(forKey: AlternateAppIconSettings.storageKey) == nil)
#expect(AlternateAppIconSettings.selected(in: defaults) == .primary)
}
@Test func resetClearsStoredPreferenceWithoutApplyingPrimary() {
let (suite, defaults) = makeDefaults()
defer { cleanup(suite) }
AlternateAppIconSettings.setSelected(.quiet, in: defaults, applySystem: false)
AlternateAppIconSettings.reset(in: defaults)
#expect(defaults.string(forKey: AlternateAppIconSettings.storageKey) == nil)
#expect(AlternateAppIconSettings.selected(in: defaults) == .primary)
}
}