mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-28 14:19:47 +00:00
Merge branch 'master' into ep/new-design
This commit is contained in:
@@ -2122,6 +2122,7 @@ struct AppSettings: Codable, Equatable {
|
||||
var privacyAskToApproveRelays: Bool? = nil
|
||||
var privacyAcceptImages: Bool? = nil
|
||||
var privacyLinkPreviews: Bool? = nil
|
||||
var privacySanitizeLinks: Bool? = nil
|
||||
var privacyShowChatPreviews: Bool? = nil
|
||||
var privacySaveLastDraft: Bool? = nil
|
||||
var privacyProtectScreen: Bool? = nil
|
||||
@@ -2157,6 +2158,7 @@ struct AppSettings: Codable, Equatable {
|
||||
if privacyAskToApproveRelays != def.privacyAskToApproveRelays { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
|
||||
if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages }
|
||||
if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews }
|
||||
if privacySanitizeLinks != def.privacySanitizeLinks { empty.privacySanitizeLinks = privacySanitizeLinks }
|
||||
if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews }
|
||||
if privacySaveLastDraft != def.privacySaveLastDraft { empty.privacySaveLastDraft = privacySaveLastDraft }
|
||||
if privacyProtectScreen != def.privacyProtectScreen { empty.privacyProtectScreen = privacyProtectScreen }
|
||||
@@ -2193,6 +2195,7 @@ struct AppSettings: Codable, Equatable {
|
||||
privacyAskToApproveRelays: true,
|
||||
privacyAcceptImages: true,
|
||||
privacyLinkPreviews: true,
|
||||
privacySanitizeLinks: false,
|
||||
privacyShowChatPreviews: true,
|
||||
privacySaveLastDraft: true,
|
||||
privacyProtectScreen: false,
|
||||
|
||||
@@ -38,6 +38,7 @@ extension AppSettings {
|
||||
privacyLinkPreviewsGroupDefault.set(val)
|
||||
def.setValue(val, forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
|
||||
}
|
||||
if let val = privacySanitizeLinks { privacySanitizeLinksGroupDefault.set(val) }
|
||||
if let val = privacyShowChatPreviews { def.setValue(val, forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) }
|
||||
if let val = privacySaveLastDraft { def.setValue(val, forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT) }
|
||||
if let val = privacyProtectScreen { def.setValue(val, forKey: DEFAULT_PRIVACY_PROTECT_SCREEN) }
|
||||
@@ -77,6 +78,7 @@ extension AppSettings {
|
||||
c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get()
|
||||
c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get()
|
||||
c.privacyLinkPreviews = privacyLinkPreviewsGroupDefault.get()
|
||||
c.privacySanitizeLinks = privacySanitizeLinksGroupDefault.get()
|
||||
c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS)
|
||||
c.privacySaveLastDraft = def.bool(forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT)
|
||||
c.privacyProtectScreen = def.bool(forKey: DEFAULT_PRIVACY_PROTECT_SCREEN)
|
||||
|
||||
@@ -237,6 +237,8 @@ public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDef
|
||||
|
||||
public let privacyAskToApproveRelaysGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS)
|
||||
|
||||
public let privacySanitizeLinksGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS)
|
||||
|
||||
public let profileImageCornerRadiusGroupDefault = Default<Double>(defaults: groupDefaults, forKey: GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)
|
||||
|
||||
public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT)
|
||||
|
||||
+5
@@ -8042,6 +8042,7 @@ data class AppSettings(
|
||||
var privacyAskToApproveRelays: Boolean? = null,
|
||||
var privacyAcceptImages: Boolean? = null,
|
||||
var privacyLinkPreviews: Boolean? = null,
|
||||
var privacySanitizeLinks: Boolean? = null,
|
||||
var privacyChatListOpenLinks: PrivacyChatListOpenLinksMode? = null,
|
||||
var privacyShowChatPreviews: Boolean? = null,
|
||||
var privacySaveLastDraft: Boolean? = null,
|
||||
@@ -8078,6 +8079,7 @@ data class AppSettings(
|
||||
if (privacyAskToApproveRelays != def.privacyAskToApproveRelays) { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
|
||||
if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages }
|
||||
if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews }
|
||||
if (privacySanitizeLinks != def.privacySanitizeLinks) { empty.privacySanitizeLinks = privacySanitizeLinks }
|
||||
if (privacyChatListOpenLinks != def.privacyChatListOpenLinks) { empty.privacyChatListOpenLinks = privacyChatListOpenLinks }
|
||||
if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews }
|
||||
if (privacySaveLastDraft != def.privacySaveLastDraft) { empty.privacySaveLastDraft = privacySaveLastDraft }
|
||||
@@ -8125,6 +8127,7 @@ data class AppSettings(
|
||||
privacyAskToApproveRelays?.let { def.privacyAskToApproveRelays.set(it) }
|
||||
privacyAcceptImages?.let { def.privacyAcceptImages.set(it) }
|
||||
privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) }
|
||||
privacySanitizeLinks?.let { def.privacySanitizeLinks.set(it) }
|
||||
privacyChatListOpenLinks?.let { def.privacyChatListOpenLinks.set(it) }
|
||||
privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) }
|
||||
privacySaveLastDraft?.let { def.privacySaveLastDraft.set(it) }
|
||||
@@ -8162,6 +8165,7 @@ data class AppSettings(
|
||||
privacyAskToApproveRelays = true,
|
||||
privacyAcceptImages = true,
|
||||
privacyLinkPreviews = true,
|
||||
privacySanitizeLinks = false,
|
||||
privacyChatListOpenLinks = PrivacyChatListOpenLinksMode.ASK,
|
||||
privacyShowChatPreviews = true,
|
||||
privacySaveLastDraft = true,
|
||||
@@ -8200,6 +8204,7 @@ data class AppSettings(
|
||||
privacyAskToApproveRelays = def.privacyAskToApproveRelays.get(),
|
||||
privacyAcceptImages = def.privacyAcceptImages.get(),
|
||||
privacyLinkPreviews = def.privacyLinkPreviews.get(),
|
||||
privacySanitizeLinks = def.privacySanitizeLinks.get(),
|
||||
privacyChatListOpenLinks = def.privacyChatListOpenLinks.get(),
|
||||
privacyShowChatPreviews = def.privacyShowChatPreviews.get(),
|
||||
privacySaveLastDraft = def.privacySaveLastDraft.get(),
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@simplex-chat/types": "^0.6.0",
|
||||
"@simplex-chat/types": "^0.7.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"commander": "^14.0.3",
|
||||
"simplex-chat": "^6.5.1",
|
||||
"simplex-chat": "^6.5.2",
|
||||
"yaml": "^2.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@simplex-chat/types",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"description": "TypeScript types for SimpleX Chat bot libraries",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simplex-chat",
|
||||
"version": "6.5.1",
|
||||
"version": "6.5.2",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
@@ -24,7 +24,7 @@
|
||||
"docs": "typedoc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@simplex-chat/types": "^0.6.0",
|
||||
"@simplex-chat/types": "^0.7.0",
|
||||
"extract-zip": "^2.0.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"node-addon-api": "^8.5.0"
|
||||
|
||||
@@ -4,7 +4,7 @@ const path = require('path');
|
||||
const extract = require('extract-zip');
|
||||
|
||||
const GITHUB_REPO = 'simplex-chat/simplex-chat-libs';
|
||||
const RELEASE_TAG = 'v6.5.1';
|
||||
const RELEASE_TAG = 'v6.5.2';
|
||||
const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase();
|
||||
|
||||
if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
simplex-chat-libs release tag we depend on.
|
||||
|
||||
Bump both together for normal releases. For wrapper-only fixes use a PEP 440
|
||||
post-release: __version__ = "6.5.1.post1", LIBS_VERSION unchanged.
|
||||
post-release: __version__ = "6.5.2.post1", LIBS_VERSION unchanged.
|
||||
"""
|
||||
|
||||
__version__ = "6.5.1" # PEP 440 — read by hatchling for wheel metadata
|
||||
LIBS_VERSION = "6.5.1" # simplex-chat-libs release tag (no 'v' prefix)
|
||||
__version__ = "6.5.2" # PEP 440 — read by hatchling for wheel metadata
|
||||
LIBS_VERSION = "6.5.2" # simplex-chat-libs release tag (no 'v' prefix)
|
||||
|
||||
@@ -41,7 +41,7 @@ def test_resolve_downloads_when_missing(tmp_path, monkeypatch):
|
||||
|
||||
monkeypatch.setattr("simplex_chat._native._download", fake_download)
|
||||
libs_dir = _resolve_libs_dir("sqlite")
|
||||
assert libs_dir == tmp_path / "simplex-chat" / "v6.5.1" / "sqlite"
|
||||
assert libs_dir == tmp_path / "simplex-chat" / "v6.5.2" / "sqlite"
|
||||
assert called["backend"] == "sqlite"
|
||||
assert (libs_dir / "libsimplex.so").exists()
|
||||
|
||||
@@ -49,7 +49,7 @@ def test_resolve_downloads_when_missing(tmp_path, monkeypatch):
|
||||
def test_resolve_uses_cache_on_second_call(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
|
||||
monkeypatch.setattr("sys.platform", "linux")
|
||||
cached = tmp_path / "simplex-chat" / "v6.5.1" / "sqlite"
|
||||
cached = tmp_path / "simplex-chat" / "v6.5.2" / "sqlite"
|
||||
cached.mkdir(parents=True)
|
||||
(cached / "libsimplex.so").touch()
|
||||
# Should NOT call _download — use the cached file.
|
||||
|
||||
@@ -42,7 +42,7 @@ def test_url_sqlite(_):
|
||||
assert (
|
||||
_libs_url("sqlite")
|
||||
== "https://github.com/simplex-chat/simplex-chat-libs/releases/download/"
|
||||
"v6.5.1/simplex-chat-libs-linux-x86_64.zip"
|
||||
"v6.5.2/simplex-chat-libs-linux-x86_64.zip"
|
||||
)
|
||||
|
||||
|
||||
@@ -51,5 +51,5 @@ def test_url_postgres(_):
|
||||
assert (
|
||||
_libs_url("postgres")
|
||||
== "https://github.com/simplex-chat/simplex-chat-libs/releases/download/"
|
||||
"v6.5.1/simplex-chat-libs-linux-x86_64-postgres.zip"
|
||||
"v6.5.2/simplex-chat-libs-linux-x86_64-postgres.zip"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# "Remove link tracking" setting does not persist across database import
|
||||
|
||||
PR: [#6977](https://github.com/simplex-chat/simplex-chat/pull/6977) · branch `nd/fix-privacy-links-import` → `master`
|
||||
|
||||
## 1. Problem statement
|
||||
|
||||
The **Settings → Privacy & security → Remove link tracking** toggle (`privacySanitizeLinks`) is silently dropped when a user moves their chat database to another device or reinstalls. Reproduction:
|
||||
|
||||
1. Device A: enable "Remove link tracking", export chat database.
|
||||
2. Device B (fresh install) or same device after re-install: import the database.
|
||||
3. Open Settings → Privacy & security on B: the toggle is **off**.
|
||||
|
||||
All three platforms are affected (Android, desktop, iOS) and any combination of source/target. Every other v6.5 "Safe web links" privacy guarantee survives the import; only "Remove link tracking" reverts.
|
||||
|
||||
## 2. Solution summary
|
||||
|
||||
The preference is stored locally only (Android `SharedPreferences`, iOS `UserDefaults` group). The cross-device transport for app settings is the `AppSettings` JSON record that travels with the database via `apiGetAppSettings` / `apiSaveAppSettings`. `privacySanitizeLinks` was absent from this record in all three layers (Haskell core, Kotlin multiplatform, Swift iOS), so it had nothing to ride on.
|
||||
|
||||
Fix: add `privacySanitizeLinks :: Maybe Bool` to the `AppSettings` record in each of the three layers, wired identically to the reference field `privacyAskToApproveRelays`. Default in all three layers is `false`, matching today's local default. The fix is strictly additive (`+18` lines, 5 files, no deletions); no schema change, no command/API change, no UI change.
|
||||
|
||||
## 3. Detailed tech design
|
||||
|
||||
### 3.1 The round-trip the fix plugs into
|
||||
|
||||
```
|
||||
Device A Device B
|
||||
───────── ─────────
|
||||
local pref store local pref store
|
||||
↑ ↑ importIntoApp()
|
||||
│ user toggles UI │
|
||||
│ AppSettings ← apiGetAppSettings(local prepareForExport)
|
||||
│ ↑
|
||||
│ archive (.zip with │
|
||||
local pref → AppSettings.current chat.db) ─────┐
|
||||
→ prepareForExport │ │
|
||||
→ apiSaveAppSettings │ │
|
||||
→ app_settings DB row ─────────┘ │
|
||||
│
|
||||
core: combineAppSettings
|
||||
stored <|> platformDefaults <|> defaults
|
||||
```
|
||||
|
||||
`AppSettings.current` reads every local pref; `prepareForExport` strips fields equal to their default (space optimisation); `apiSaveAppSettings` writes the JSON into the `app_settings` table of the chat DB, which travels inside the archive. On import, the receiving client runs `apiGetAppSettings(local.prepareForExport())`; the core merges stored ⟶ local-platform ⟶ hardcoded-defaults with `Alternative` (`<|>`) and returns the result; the client's `importIntoApp` applies any non-null fields to its local store.
|
||||
|
||||
A field that is **absent from `AppSettings`** at any of the three layers never enters this pipeline and is therefore lost on import. `privacySanitizeLinks` was such a field.
|
||||
|
||||
### 3.2 Three-layer parity
|
||||
|
||||
The three `AppSettings` definitions must agree on every field name, default value, and the four operations:
|
||||
|
||||
| Operation | Haskell | Kotlin | Swift |
|
||||
|---|---|---|---|
|
||||
| field declaration | `data AppSettings` (`src/Simplex/Chat/AppSettings.hs:28`) | `data class AppSettings` (`SimpleXAPI.kt:8038`) | `struct AppSettings` (`AppAPITypes.swift:2118`) |
|
||||
| default | `defaultAppSettings` (`AppSettings.hs:79`) | `defaults` (`SimpleXAPI.kt:8157`) | `defaults` (`AppAPITypes.swift:2188`) |
|
||||
| "missing key" parse default | `defaultParseAppSettings` (`AppSettings.hs:116`) | implicit `null` | implicit `nil` |
|
||||
| merge fallback | `combineAppSettings` (`AppSettings.hs:153`) | n/a (only one source) | n/a |
|
||||
| JSON parser | hand-written `parseJSON` (`AppSettings.hs:207`) | `@Serializable` derived | `Codable` derived |
|
||||
| read-from-local | n/a (clients send it) | `AppSettings.current` (`SimpleXAPI.kt:8193`) | `AppSettings.current` (`AppSettings.swift:71`) |
|
||||
| write-to-local | n/a (clients apply it) | `importIntoApp` (`SimpleXAPI.kt:8110`) | `updateIosGroupDefaults` / `init from cfg` (`AppSettings.swift:13`) |
|
||||
| serialize-only-non-default | n/a | `prepareForExport` (`SimpleXAPI.kt:8072`) | `prepareForExport` (`AppAPITypes.swift:2151`) |
|
||||
|
||||
The fix adds one line to every cell that exists for `privacyAskToApproveRelays`. Default value is `false` (matches `mkBoolPreference(..., false)` and the `registerGroupDefaults` entry).
|
||||
|
||||
### 3.3 Round-trip correctness — case analysis
|
||||
|
||||
The core's `combineAppSettings = stored <|> platformDefaults <|> defaultAppSettings` (with `Alternative` on `Maybe`) means: take the stored value if present, else what the client said its default is, else the hardcoded default. The client's `prepareForExport` only includes a field when it differs from the client's `defaults`. With both `defaults` set to `false`:
|
||||
|
||||
| Case | Archive carries | Local pref before | platformDefaults sent | Merged | Result |
|
||||
|---|---|---|---|---|---|
|
||||
| New archive, source had on | `Just true` | false | `Nothing` (default) | `Just true` | **on** ✓ |
|
||||
| New archive, source had off (default) | `Nothing` (stripped) | false | `Nothing` | `Just false` (from defaults) | **off** ✓ |
|
||||
| New archive, source had off | `Nothing` | true (local toggled) | `Just true` | `Just true` | **on** (local wins, archive silent) ✓ |
|
||||
| Old archive (pre-fix) | field unknown | false | `Nothing` | `Just false` | **off** (unchanged from before fix) |
|
||||
| Old archive | field unknown | true | `Just true` | `Just true` | **on** (local preserved) ✓ |
|
||||
| Cross-platform | `Just true` | false | `Nothing` | `Just true` | **on** ✓ |
|
||||
|
||||
The only "interesting" semantic — *archive silent on the field while local has it on* — preserves local. This matches how every other field in `AppSettings` behaves and matches user intent ("I toggled it on this device, then imported some old archive — keep it on").
|
||||
|
||||
### 3.4 Edge cases verified
|
||||
|
||||
- **Downgrade then upgrade.** New code → toggle on → export. Imported on *old* code: `parseJSON` ignores unknown keys, DB row is rewritten without the field. Re-upgrade: field absent, falls through to `Just false`. This is the standard "old client drops new fields" semantics for every prior AppSettings addition; not introduced by this PR.
|
||||
|
||||
- **iOS `BoolDefault` before `set` is ever called.** `apps/ios/SimpleXChat/AppGroup.swift:100` already registers `GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS: false` in `registerGroupDefaults`. So `privacySanitizeLinksGroupDefault.get()` returns `false` on first read — no NaN/nil sentinel risk.
|
||||
|
||||
- **JSON field ordering.** `deriveToJSON defaultJSON` uses record-field order; new field is inserted between `privacyLinkPreviews` and `privacyShowChatPreviews`, shifting subsequent keys. No external consumer compares the JSON byte-for-byte; the existing `testAppSettings` test compares `J.encode defaultAppSettings` on both sides of the wire and so is self-consistent under the addition.
|
||||
|
||||
- **`omitNothingFields = True`.** The Haskell `defaultJSON` config (`Simplex.Messaging.Parsers`) strips `Nothing` fields from JSON output, so `defaultParseAppSettings` (every field `Nothing`) does not pollute archives or wire payloads when used as a fallback.
|
||||
|
||||
- **iOS NSE / SE extensions.** Neither references `privacySanitizeLinks`. No additional wiring required.
|
||||
|
||||
### 3.5 What was deliberately not done
|
||||
|
||||
- **Flipping the *user-facing* default to `true`.** Other privacy fields in `defaultAppSettings` are `Just True` (encrypt local files, ask to approve relays). "Remove link tracking" remains `Just False` because the local pref default (`mkBoolPreference(..., false)`, iOS `registerGroupDefaults: false`) is `false`. Aligning the `AppSettings` default with the local default keeps the `prepareForExport` "differs-from-default" comparison consistent — otherwise off-by-default users would suddenly serialise `false` everywhere and on-by-default users would serialise nothing, inverting the wire shape. Whether the *product* default should be flipped to on is a separate question for a separate change.
|
||||
|
||||
- **Adding `apiSaveAppSettings` on toggle.** Toggling the pref in `PrivacySettings.kt` writes only to shared prefs; the DB's `app_settings` row stays stale until a separate trigger (theme change, export, migration) syncs. The export and migration paths already call `apiSaveAppSettings(AppSettings.current.prepareForExport())` immediately before producing the archive, so every UI-initiated export captures the current value. Plugging the sync into every toggle is a broader change affecting every AppSettings field equally — out of scope.
|
||||
|
||||
- **Fixing `privacyChatListOpenLinks`.** The Kotlin `AppSettings` declares it (`SimpleXAPI.kt:8046`); the Haskell record and the Swift struct do not. Same failure mode as the bug being fixed here — almost certainly does not persist across Android-to-Android imports. Out of scope; should be tracked separately.
|
||||
|
||||
- **Adding a targeted test.** The existing `testAppSettings` exercises a JSON round-trip with `defaultAppSettings`, so the new field rides through implicitly. A field-specific test (`defaultAppSettings { privacySanitizeLinks = Just True }`) would tighten coverage against a future client dropping the field; recommended as a small follow-up.
|
||||
|
||||
## 4. Detailed implementation plan
|
||||
|
||||
### 4.1 Files touched
|
||||
|
||||
| File | Δ | Purpose |
|
||||
|---|---|---|
|
||||
| `src/Simplex/Chat/AppSettings.hs` | +6 / 0 | record field, `defaultAppSettings`, `defaultParseAppSettings`, `combineAppSettings`, JSON parser line, record reassembly |
|
||||
| `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt` | +5 / 0 | data class field, `prepareForExport`, `importIntoApp`, `defaults`, `current` |
|
||||
| `apps/ios/Shared/Model/AppAPITypes.swift` | +3 / 0 | struct field, `prepareForExport`, `defaults` |
|
||||
| `apps/ios/SimpleXChat/AppGroup.swift` | +2 / 0 | new `privacySanitizeLinksGroupDefault: BoolDefault` next to existing privacy defaults |
|
||||
| `apps/ios/Shared/Views/UserSettings/AppSettings.swift` | +2 / 0 | import side (`set`), export side (`get`) |
|
||||
|
||||
Total: 5 files, +18 / 0. No deletions.
|
||||
|
||||
### 4.2 Step-by-step (commit `15457a903`)
|
||||
|
||||
1. **`AppSettings.hs`** — add `privacySanitizeLinks :: Maybe Bool` to the record (between `privacyLinkPreviews` and `privacyShowChatPreviews`); set `Just False` in `defaultAppSettings`; `Nothing` in `defaultParseAppSettings`; `p privacySanitizeLinks` in `combineAppSettings`; `privacySanitizeLinks <- p "privacySanitizeLinks"` in `parseJSON`; add to record reassembly. Field position consistent with name groupings.
|
||||
|
||||
2. **`SimpleXAPI.kt`** — same insertions in `data class AppSettings`, `prepareForExport`, `importIntoApp`, `defaults`, `current`. Local pref already exists (`SimpleXAPI.kt:126`).
|
||||
|
||||
3. **`AppAPITypes.swift`** — same insertions in `struct AppSettings`, `prepareForExport`, `defaults`.
|
||||
|
||||
4. **`AppGroup.swift`** — add `public let privacySanitizeLinksGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS)`. The key constant (line 31) and registered default `false` (line 100) already exist; only the typed wrapper for non-`@AppStorage` access was missing.
|
||||
|
||||
5. **`AppSettings.swift` (iOS view extension)** — import side: `if let val = privacySanitizeLinks { privacySanitizeLinksGroupDefault.set(val) }`. Export side: `c.privacySanitizeLinks = privacySanitizeLinksGroupDefault.get()`.
|
||||
|
||||
### 4.3 Verification
|
||||
|
||||
- Haskell `testAppSettings` (`tests/ChatTests/Direct.hs:2768`) covers the JSON round-trip through `defaultAppSettings`; the new field flows through both sides of the equality, so existing assertions hold.
|
||||
- Manual test plan (in PR description):
|
||||
1. Enable on Android, export DB, import on a second Android device — toggle stays on.
|
||||
2. Enable on iOS, export, import on a second iOS device — toggle stays on.
|
||||
3. Enable on desktop, export, fresh-install + import — toggle stays on.
|
||||
4. Cross-platform: export from Android, import on iOS, and vice versa — toggle preserved.
|
||||
5. Fresh install with no archive — toggle defaults to off (unchanged).
|
||||
|
||||
### 4.4 Risk and rollback
|
||||
|
||||
- **Blast radius**: the `AppSettings` JSON payload. Every other field is untouched (positional inserts, no reordering of existing fields beyond the natural shift).
|
||||
- **Backwards compatibility**: old clients (no field) parsing new JSON ignore the key. New clients (with field) parsing old JSON see `Nothing`, fall through to `defaultAppSettings` and the local pref is set to its default. Either direction is safe.
|
||||
- **Rollback**: `git revert 15457a903`. Restores pre-fix behaviour (the field-loss bug returns).
|
||||
|
||||
## 5. Why this specific shape
|
||||
|
||||
- The bug has exactly one cause: a missing field in the round-trip payload. The smallest fix is to add the field. Anything larger (e.g. broadening `importIntoApp` to scan all shared prefs, or pinning the value in a side channel) would be a structural change that does not improve correctness.
|
||||
- The `<|>` merge in `combineAppSettings` already gives the right behaviour for every edge case (archive-silent local-set, fresh install, downgrade) once the field exists. No new merge logic needed.
|
||||
- The default `false` is forced: any other choice would either contradict the local pref default (`mkBoolPreference(..., false)`, iOS `registerGroupDefaults: false`) or invert the wire shape of `prepareForExport`.
|
||||
- Final PR is 5 files, +18 / 0. Three of those files are the three `AppSettings` records; the other two are the iOS wiring the new field needs in order to read and write its group default. No other file in the codebase needed touching.
|
||||
@@ -38,6 +38,32 @@
|
||||
</description>
|
||||
|
||||
<releases>
|
||||
<release version="6.5.2" date="2026-05-15">
|
||||
<url type="details">https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html</url>
|
||||
<description>
|
||||
<p>New in v6.5.2:</p>
|
||||
<ul>
|
||||
<li>allow deleting messages from channel history without time limit.</li>
|
||||
</ul>
|
||||
<p>New in v6.5:</p>
|
||||
<p>Public channels - speak freely!</p>
|
||||
<ul>
|
||||
<li>Reliability: many relays per channel.</li>
|
||||
<li>Ownership: you can run your own relays.</li>
|
||||
<li>Security: owners hold channel keys.</li>
|
||||
<li>Privacy: for owners and subscribers.</li>
|
||||
</ul>
|
||||
<p>Easier to invite your friends: we made connecting simpler for new users.</p>
|
||||
<p>Safe web links:</p>
|
||||
<ul>
|
||||
<li>opt-in to send link previews.</li>
|
||||
<li>use SOCKS proxy for previews (if enabled).</li>
|
||||
<li>prevent hyperlink phishing.</li>
|
||||
<li>remove link tracking.</li>
|
||||
</ul>
|
||||
<p>Non-profit governance: to make SimpleX Network last.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="6.5.1" date="2026-05-02">
|
||||
<url type="details">https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html</url>
|
||||
<description>
|
||||
|
||||
@@ -33,6 +33,7 @@ data AppSettings = AppSettings
|
||||
privacyAskToApproveRelays :: Maybe Bool,
|
||||
privacyAcceptImages :: Maybe Bool,
|
||||
privacyLinkPreviews :: Maybe Bool,
|
||||
privacySanitizeLinks :: Maybe Bool,
|
||||
privacyShowChatPreviews :: Maybe Bool,
|
||||
privacySaveLastDraft :: Maybe Bool,
|
||||
privacyProtectScreen :: Maybe Bool,
|
||||
@@ -83,6 +84,7 @@ defaultAppSettings =
|
||||
privacyAskToApproveRelays = Just True,
|
||||
privacyAcceptImages = Just True,
|
||||
privacyLinkPreviews = Just True,
|
||||
privacySanitizeLinks = Just False,
|
||||
privacyShowChatPreviews = Just True,
|
||||
privacySaveLastDraft = Just True,
|
||||
privacyProtectScreen = Just False,
|
||||
@@ -120,6 +122,7 @@ defaultParseAppSettings =
|
||||
privacyAskToApproveRelays = Nothing,
|
||||
privacyAcceptImages = Nothing,
|
||||
privacyLinkPreviews = Nothing,
|
||||
privacySanitizeLinks = Nothing,
|
||||
privacyShowChatPreviews = Nothing,
|
||||
privacySaveLastDraft = Nothing,
|
||||
privacyProtectScreen = Nothing,
|
||||
@@ -157,6 +160,7 @@ combineAppSettings platformDefaults storedSettings =
|
||||
privacyAskToApproveRelays = p privacyAskToApproveRelays,
|
||||
privacyAcceptImages = p privacyAcceptImages,
|
||||
privacyLinkPreviews = p privacyLinkPreviews,
|
||||
privacySanitizeLinks = p privacySanitizeLinks,
|
||||
privacyShowChatPreviews = p privacyShowChatPreviews,
|
||||
privacySaveLastDraft = p privacySaveLastDraft,
|
||||
privacyProtectScreen = p privacyProtectScreen,
|
||||
@@ -210,6 +214,7 @@ instance FromJSON AppSettings where
|
||||
privacyAskToApproveRelays <- p "privacyAskToApproveRelays"
|
||||
privacyAcceptImages <- p "privacyAcceptImages"
|
||||
privacyLinkPreviews <- p "privacyLinkPreviews"
|
||||
privacySanitizeLinks <- p "privacySanitizeLinks"
|
||||
privacyShowChatPreviews <- p "privacyShowChatPreviews"
|
||||
privacySaveLastDraft <- p "privacySaveLastDraft"
|
||||
privacyProtectScreen <- p "privacyProtectScreen"
|
||||
@@ -244,6 +249,7 @@ instance FromJSON AppSettings where
|
||||
privacyAskToApproveRelays,
|
||||
privacyAcceptImages,
|
||||
privacyLinkPreviews,
|
||||
privacySanitizeLinks,
|
||||
privacyShowChatPreviews,
|
||||
privacySaveLastDraft,
|
||||
privacyProtectScreen,
|
||||
|
||||
Reference in New Issue
Block a user