diff --git a/bitchat/Assets.xcassets/AppIconNeutral.appiconset/Contents.json b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/Contents.json new file mode 100644 index 00000000..16cd38f5 --- /dev/null +++ b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/Contents.json @@ -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 + } +} diff --git a/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_1024x1024.png b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_1024x1024.png new file mode 100644 index 00000000..4f638863 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_1024x1024.png differ diff --git a/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_128x128.png b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_128x128.png new file mode 100644 index 00000000..e1fc340f Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_128x128.png differ diff --git a/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_128x128@2x.png b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_128x128@2x.png new file mode 100644 index 00000000..4fe7f096 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_128x128@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_16x16.png b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_16x16.png new file mode 100644 index 00000000..10fea999 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_16x16.png differ diff --git a/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_16x16@2x.png b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_16x16@2x.png new file mode 100644 index 00000000..cb6e4cff Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_16x16@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_256x256.png b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_256x256.png new file mode 100644 index 00000000..4fe7f096 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_256x256.png differ diff --git a/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_256x256@2x.png b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_256x256@2x.png new file mode 100644 index 00000000..b06b2710 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_256x256@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_32x32.png b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_32x32.png new file mode 100644 index 00000000..cb6e4cff Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_32x32.png differ diff --git a/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_32x32@2x.png b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_32x32@2x.png new file mode 100644 index 00000000..cb1830b4 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_32x32@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_512x512.png b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_512x512.png new file mode 100644 index 00000000..b06b2710 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_512x512.png differ diff --git a/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_512x512@2x.png b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_512x512@2x.png new file mode 100644 index 00000000..45896bcd Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_512x512@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIconNotes.appiconset/Contents.json b/bitchat/Assets.xcassets/AppIconNotes.appiconset/Contents.json new file mode 100644 index 00000000..16cd38f5 --- /dev/null +++ b/bitchat/Assets.xcassets/AppIconNotes.appiconset/Contents.json @@ -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 + } +} diff --git a/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_1024x1024.png b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_1024x1024.png new file mode 100644 index 00000000..ff06cc93 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_1024x1024.png differ diff --git a/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_128x128.png b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_128x128.png new file mode 100644 index 00000000..7923dbfb Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_128x128.png differ diff --git a/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_128x128@2x.png b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_128x128@2x.png new file mode 100644 index 00000000..ee011908 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_128x128@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_16x16.png b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_16x16.png new file mode 100644 index 00000000..5bbfedfc Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_16x16.png differ diff --git a/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_16x16@2x.png b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_16x16@2x.png new file mode 100644 index 00000000..05ee03f2 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_16x16@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_256x256.png b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_256x256.png new file mode 100644 index 00000000..ee011908 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_256x256.png differ diff --git a/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_256x256@2x.png b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_256x256@2x.png new file mode 100644 index 00000000..d47b7871 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_256x256@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_32x32.png b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_32x32.png new file mode 100644 index 00000000..05ee03f2 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_32x32.png differ diff --git a/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_32x32@2x.png b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_32x32@2x.png new file mode 100644 index 00000000..a29fa027 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_32x32@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_512x512.png b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_512x512.png new file mode 100644 index 00000000..d47b7871 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_512x512.png differ diff --git a/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_512x512@2x.png b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_512x512@2x.png new file mode 100644 index 00000000..86b26a1f Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_512x512@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIconQuiet.appiconset/Contents.json b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/Contents.json new file mode 100644 index 00000000..16cd38f5 --- /dev/null +++ b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/Contents.json @@ -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 + } +} diff --git a/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_1024x1024.png b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_1024x1024.png new file mode 100644 index 00000000..22819b0f Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_1024x1024.png differ diff --git a/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_128x128.png b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_128x128.png new file mode 100644 index 00000000..8e1243e0 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_128x128.png differ diff --git a/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_128x128@2x.png b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_128x128@2x.png new file mode 100644 index 00000000..bd75bde0 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_128x128@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_16x16.png b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_16x16.png new file mode 100644 index 00000000..dfc205ee Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_16x16.png differ diff --git a/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_16x16@2x.png b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_16x16@2x.png new file mode 100644 index 00000000..8d3bde04 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_16x16@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_256x256.png b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_256x256.png new file mode 100644 index 00000000..bd75bde0 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_256x256.png differ diff --git a/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_256x256@2x.png b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_256x256@2x.png new file mode 100644 index 00000000..9704fc30 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_256x256@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_32x32.png b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_32x32.png new file mode 100644 index 00000000..8d3bde04 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_32x32.png differ diff --git a/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_32x32@2x.png b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_32x32@2x.png new file mode 100644 index 00000000..9cfab6cc Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_32x32@2x.png differ diff --git a/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_512x512.png b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_512x512.png new file mode 100644 index 00000000..9704fc30 Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_512x512.png differ diff --git a/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_512x512@2x.png b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_512x512@2x.png new file mode 100644 index 00000000..927e400e Binary files /dev/null and b/bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_512x512@2x.png differ diff --git a/bitchat/BitchatApp.swift b/bitchat/BitchatApp.swift index cf2e1bd1..44b2a80b 100644 --- a/bitchat/BitchatApp.swift +++ b/bitchat/BitchatApp.swift @@ -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) diff --git a/bitchat/Localizable.xcstrings b/bitchat/Localizable.xcstrings index f869ab85..a1ce3a9b 100644 --- a/bitchat/Localizable.xcstrings +++ b/bitchat/Localizable.xcstrings @@ -78651,6 +78651,1116 @@ } } }, + "app_info.settings.icon.neutral" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "محايد" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "নিউট্রাল" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "neutral" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "neutral" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "neutro" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "خنثی" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "neutral" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "neutre" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ניטרלי" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "न्यूट्रल" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "netral" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "neutra" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ニュートラル" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "뉴트럴" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "neutral" + } + }, + "ne" : { + "stringUnit" : { + "state" : "translated", + "value" : "न्यूट्रल" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "neutraal" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "neutralna" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "neutro" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "neutro" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "нейтральная" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "neutral" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "நியூட்ரல்" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "กลาง" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "nötr" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "нейтральна" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "نیوٹرل" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "trung tính" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "中性" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "中性" + } + } + } + }, + "app_info.settings.icon.notes" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "ملاحظات" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "নোটস" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "notizen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "notes" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "notas" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "یادداشت‌ها" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "notes" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "notes" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "פתקים" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "नोट्स" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "catatan" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "note" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メモ" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "메모" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "nota" + } + }, + "ne" : { + "stringUnit" : { + "state" : "translated", + "value" : "नोटहरू" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "notities" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "notatki" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "notas" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "notas" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "заметки" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "anteckningar" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "குறிப்புகள்" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "โน้ต" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "notlar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "нотатки" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "نوٹس" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ghi chú" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "备忘录" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "備忘錄" + } + } + } + }, + "app_info.settings.icon.primary" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "ne" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "bitchat" + } + } + } + }, + "app_info.settings.icon.quiet" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "هادئ" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "কোয়াইয়েট" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "leise" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "quiet" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "discreto" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "آرام" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "quiet" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "discret" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שקטה" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्वाइएट" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "tenang" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "discreta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クワイエット" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "콰이어트" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "senyap" + } + }, + "ne" : { + "stringUnit" : { + "state" : "translated", + "value" : "शान्त" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "stil" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "cicha" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "discreto" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "discreto" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "тихая" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "tyst" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "அமைதி" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "เงียบ" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "sessiz" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "тиха" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "خاموش" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "yên" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "低调" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "低調" + } + } + } + }, + "app_info.settings.icon.subtitle" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أيقونات اختيارية منخفضة البروفايل. تسمية الشاشة الرئيسية ما زالت “bitchat”، والاسم الحقيقي يظهر في الإعدادات والبحث — يتغير الشكل فقط." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "ঐচ্ছিক লো-প্রোফাইল আইকন। হোম-স্ক্রিন লেবেল এখনও “bitchat”, আর আসল নাম সেটিংস ও সার্চে থাকে — শুধু গ্লিফ বদলায়।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "optionale unauffällige icons. die home-screen-beschriftung lautet weiterhin „bitchat“, und der echte name erscheint weiter in einstellungen und suche — geändert wird nur das symbol." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "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." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "iconos discretos opcionales. la etiqueta en la pantalla de inicio sigue diciendo “bitchat”, y el nombre real sigue en ajustes y búsqueda: solo cambia el glifo." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "آیکون‌های اختیاری کم‌جلوه. برچسب صفحه اصلی هنوز «bitchat» است و نام واقعی در تنظیمات و جستجو دیده می‌شود — فقط شکلک عوض می‌شود." + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "opsyonal na low-profile na icon. ang label sa home screen ay “bitchat” pa rin, at ang tunay na pangalan ay nasa settings at search pa rin — glyph lang ang nagbabago." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "icônes discrètes optionnelles. le libellé de l’écran d’accueil reste « bitchat », et le vrai nom apparaît encore dans réglages et recherche — seul le glyphe change." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אייקונים אופציונליים בעלי פרופיל נמוך. התווית במסך הבית עדיין “bitchat”, והשם האמיתי עדיין מופיע בהגדרות ובחיפוש — רק הגליף משתנה." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "वैकल्पिक लो-प्रोफ़ाइल आइकन। होम-स्क्रीन लेबल अभी भी “bitchat” है, और असली नाम सेटिंग्स व खोज में दिखता है — केवल ग्लिफ़ बदलता है।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "ikon low-profile opsional. label layar beranda tetap “bitchat”, dan nama asli tetap di pengaturan serta pencarian — hanya glif yang berubah." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "icone discrete opzionali. l’etichetta sulla home resta “bitchat”, e il nome reale resta in impostazioni e ricerca — cambia solo il glifo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "任意の控えめなアイコン。ホーム画面のラベルは引き続き「bitchat」で、設定や検索にも本名が出ます。変わるのはアイコン画像だけです。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "선택적 저프로필 아이콘. 홈 화면 라벨은 여전히 “bitchat”이고, 설정과 검색에도 실제 이름이 보입니다. 바뀌는 것은 글자 모양뿐입니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "ikon berprofil rendah pilihan. label skrin utama masih “bitchat”, dan nama sebenar masih dalam tetapan serta carian — hanya glif berubah." + } + }, + "ne" : { + "stringUnit" : { + "state" : "translated", + "value" : "वैकल्पिक लो-प्रोफाइल आइकन। होम-स्क्रिन लेबल अझै “bitchat” छ, र वास्तविक नाम सेटिङ र खोजमा देखिन्छ — ग्लिफ मात्र परिवर्तन हुन्छ।" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "optionele discrete pictogrammen. het label op het beginscherm blijft “bitchat”, en de echte naam blijft in instellingen en zoeken — alleen het icoon verandert." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "opcjonalne dyskretne ikony. etykieta na ekranie głównym nadal brzmi „bitchat”, a prawdziwa nazwa nadal widać w ustawieniach i wyszukiwaniu — zmienia się tylko glif." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "ícones discretos opcionais. o rótulo no ecrã principal continua “bitchat”, e o nome real continua em definições e pesquisa — só muda o glifo." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "ícones discretos opcionais. o rótulo na tela inicial continua “bitchat”, e o nome real continua em ajustes e busca — só muda o glifo." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "необязательные скромные значки. подпись на домашнем экране по-прежнему «bitchat», а настоящее имя видно в настройках и поиске — меняется только глиф." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "valfria diskreta ikoner. etiketten på hemskärmen står fortfarande “bitchat”, och det riktiga namnet syns fortfarande i inställningar och sök — bara glyfen ändras." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "விருப்ப குறைந்த தோற்ற ஐகான்கள். முகப்புத் திரை லேபிள் இன்னும் “bitchat”; உண்மைப் பெயர் அமைப்புகள் மற்றும் தேடலில் தெரியும் — கிளிஃப் மட்டும் மாறும்." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ไอคอนโปรไฟล์ต่ำแบบเลือกได้ ป้ายบนโฮมสกรีนยังเป็น “bitchat” และชื่อจริงยังโชว์ในตั้งค่าและการค้นหา — เปลี่ยนเฉพาะรูปไอคอน" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "isteğe bağlı düşük profilli simgeler. ana ekran etiketi hâlâ “bitchat” yazar; gerçek ad ayarlar ve aramada görünür — yalnızca glif değişir." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "необов’язкові стримані іконки. підпис на головному екрані й далі «bitchat», а справжня назва лишається в параметрах і пошуку — змінюється лише гліф." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "اختیاری لو-پروفائل آئیکنز۔ ہوم اسکرین لیبل اب بھی “bitchat” ہے، اور اصل نام سیٹنگز اور تلاش میں دکھتا ہے — صرف گلف بدلتا ہے۔" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "biểu tượng thấp profile tùy chọn. nhãn màn hình chính vẫn là “bitchat”, và tên thật vẫn hiện trong cài đặt và tìm kiếm — chỉ đổi glyph." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "可选低调图标。主屏幕标签仍显示“bitchat”,设置和搜索里仍是真名——只改图标字形。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "可選低調圖示。主畫面標籤仍顯示「bitchat」,設定與搜尋仍是真名——只改圖示字形。" + } + } + } + }, + "app_info.settings.icon.title" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أيقونة الشاشة الرئيسية" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "হোম স্ক্রিন আইকন" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "HOME-SCREEN-SYMBOL" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "HOME SCREEN ICON" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "ICONO DE INICIO" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "آیکون صفحه اصلی" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "ICON SA HOME SCREEN" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ICÔNE ÉCRAN D’ACCUEIL" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "סמל מסך הבית" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "होम स्क्रीन आइकन" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "IKON LAYAR BERANDA" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "ICONA SCHERMATA HOME" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ホーム画面アイコン" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "홈 화면 아이콘" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "IKON SKRIN UTAMA" + } + }, + "ne" : { + "stringUnit" : { + "state" : "translated", + "value" : "होम स्क्रिन आइकन" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "BEGINSCHERMICOON" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "IKONA EKRANU GŁÓWNEGO" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "ÍCONE DO ECRÃ PRINCIPAL" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "ÍCONE DA TELA INICIAL" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ЗНАЧОК ДОМАШНЕГО ЭКРАНА" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "HEMSKÄRMSIKON" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "முகப்புத் திரை ஐகான்" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ไอคอนหน้าจอโฮม" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ANA EKRAN SİMGESİ" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "ІКОНКА ГОЛОВНОГО ЕКРАНА" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ہوم اسکرین آئیکن" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "BIỂU TƯỢNG MÀN HÌNH CHÍNH" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "主屏幕图标" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "主畫面圖示" + } + } + } + }, "app_info.settings.language.picker_label" : { "comment" : "Label of the app language picker row in settings", "extractionState" : "manual", diff --git a/bitchat/Services/AlternateAppIconSettings.swift b/bitchat/Services/AlternateAppIconSettings.swift new file mode 100644 index 00000000..4938d27b --- /dev/null +++ b/bitchat/Services/AlternateAppIconSettings.swift @@ -0,0 +1,121 @@ +// +// AlternateAppIconSettings.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +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 + } +} diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 12adfda9..96328ad8 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -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() diff --git a/bitchat/Views/AppInfoView.swift b/bitchat/Views/AppInfoView.swift index cb99fbac..889f45b1 100644 --- a/bitchat/Views/AppInfoView.swift +++ b/bitchat/Views/AppInfoView.swift @@ -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) { diff --git a/bitchatTests/Services/AlternateAppIconSettingsTests.swift b/bitchatTests/Services/AlternateAppIconSettingsTests.swift new file mode 100644 index 00000000..39d25e7d --- /dev/null +++ b/bitchatTests/Services/AlternateAppIconSettingsTests.swift @@ -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) + } +}