mirror of
https://github.com/ratspeak/Ratspeak
synced 2026-08-12 18:07:35 -04:00
ui: refine text scaling and activity privacy
This commit is contained in:
parent
6a2dc011dc
commit
860d814ae6
15 changed files with 597 additions and 152 deletions
|
|
@ -953,26 +953,39 @@ pub async fn set_auto_announce(state: State<'_, Arc<AppState>>, interval: u64) -
|
|||
|
||||
#[tauri::command]
|
||||
pub async fn api_app_settings(state: State<'_, Arc<AppState>>) -> AppResult<Value> {
|
||||
let (hw_timeout, developer_mode, window_decorations, channel_hosting_enabled) =
|
||||
db::spawn_db(state.db.clone(), |p| {
|
||||
let hw_timeout = db::get_setting(&p, "hardware_session_timeout")
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
let developer_mode =
|
||||
db::get_setting(&p, "developer_mode_enabled").is_some_and(|v| v == "true");
|
||||
let window_decorations =
|
||||
db::get_setting(&p, "window_decorations").unwrap_or_else(|| "auto".to_string());
|
||||
let channel_hosting_enabled =
|
||||
ratspeak_runtime::channel_hub::channel_hosting_enabled(&p);
|
||||
(
|
||||
hw_timeout,
|
||||
developer_mode,
|
||||
window_decorations,
|
||||
channel_hosting_enabled,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap_or((0, false, "auto".to_string(), false));
|
||||
let (
|
||||
hw_timeout,
|
||||
developer_mode,
|
||||
window_decorations,
|
||||
channel_hosting_enabled,
|
||||
activity_identity_protection,
|
||||
text_scale_percent,
|
||||
) = db::spawn_db(state.db.clone(), |p| {
|
||||
let hw_timeout = db::get_setting(&p, "hardware_session_timeout")
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
let developer_mode =
|
||||
db::get_setting(&p, "developer_mode_enabled").is_some_and(|v| v == "true");
|
||||
let window_decorations =
|
||||
db::get_setting(&p, "window_decorations").unwrap_or_else(|| "auto".to_string());
|
||||
let channel_hosting_enabled = ratspeak_runtime::channel_hub::channel_hosting_enabled(&p);
|
||||
let activity_identity_protection = db::get_setting(&p, "activity_identity_protection")
|
||||
.is_none_or(|value| value != "false");
|
||||
let text_scale_percent = db::get_setting(&p, "text_scale_percent")
|
||||
.and_then(|value| value.parse::<u16>().ok())
|
||||
.map(|value| (value.clamp(100, 140) + 5) / 10 * 10)
|
||||
.unwrap_or(100);
|
||||
(
|
||||
hw_timeout,
|
||||
developer_mode,
|
||||
window_decorations,
|
||||
channel_hosting_enabled,
|
||||
activity_identity_protection,
|
||||
text_scale_percent,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap_or((0, false, "auto".to_string(), false, true, 100));
|
||||
Ok(json!({
|
||||
"auto_announce_interval": *state.announce_interval_rx.borrow(),
|
||||
"announce_ratspeak_usage": state.announce_ratspeak_usage_enabled(),
|
||||
|
|
@ -981,9 +994,53 @@ pub async fn api_app_settings(state: State<'_, Arc<AppState>>) -> AppResult<Valu
|
|||
"developer_mode": developer_mode,
|
||||
"window_decorations": window_decorations,
|
||||
"channel_hosting_enabled": channel_hosting_enabled,
|
||||
"activity_identity_protection": activity_identity_protection,
|
||||
"text_scale_percent": text_scale_percent,
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_text_scale(state: State<'_, Arc<AppState>>, percent: u16) -> AppResult<Value> {
|
||||
let percent = (percent.clamp(100, 140) + 5) / 10 * 10;
|
||||
db::spawn_db(state.db.clone(), move |p| {
|
||||
db::try_set_setting(&p, "text_scale_percent", &percent.to_string())
|
||||
})
|
||||
.await
|
||||
.map_err(|_| AppError::internal("set_text_scale db task panicked"))?
|
||||
.map_err(|error| {
|
||||
AppError::database_unavailable(format!("Failed to save text size: {error}"))
|
||||
})?;
|
||||
state.emit_to_all(
|
||||
"app_settings_updated",
|
||||
json!({ "text_scale_percent": percent }),
|
||||
);
|
||||
Ok(json!({ "percent": percent }))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_activity_identity_protection(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
enabled: bool,
|
||||
) -> AppResult<Value> {
|
||||
db::spawn_db(state.db.clone(), move |p| {
|
||||
db::try_set_setting(
|
||||
&p,
|
||||
"activity_identity_protection",
|
||||
if enabled { "true" } else { "false" },
|
||||
)
|
||||
})
|
||||
.await
|
||||
.map_err(|_| AppError::internal("set_activity_identity_protection db task panicked"))?
|
||||
.map_err(|error| {
|
||||
AppError::database_unavailable(format!("Failed to save Activity privacy setting: {error}"))
|
||||
})?;
|
||||
state.emit_to_all(
|
||||
"app_settings_updated",
|
||||
json!({ "activity_identity_protection": enabled }),
|
||||
);
|
||||
Ok(json!({ "enabled": enabled }))
|
||||
}
|
||||
|
||||
/// Developer mode lives in SQLite, not WebView localStorage: WKWebView does
|
||||
/// not reliably persist localStorage for custom-scheme origins (macOS/iOS).
|
||||
#[tauri::command]
|
||||
|
|
|
|||
|
|
@ -1148,6 +1148,65 @@ fn privacy_announce_usage_setting_is_wired() {
|
|||
assert!(!reset_body.contains("\"settings\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn activity_identity_protection_is_default_on_durable_and_event_scoped() {
|
||||
let root = repo_root();
|
||||
let index = read_source(root.join("dashboard/index.html")).expect("dashboard index");
|
||||
let settings = read_source(root.join("dashboard/static/js/settings.js")).expect("settings js");
|
||||
let activity = read_source(root.join("dashboard/static/js/activity.js")).expect("activity js");
|
||||
let interfaces = read_source(root.join("crates/ratspeak-tauri/src/commands/interfaces.rs"))
|
||||
.expect("interfaces commands");
|
||||
let tauri_lib = read_source(root.join("src-tauri/src/lib.rs")).expect("tauri lib");
|
||||
|
||||
assert!(index.contains("Protect Activity identities"));
|
||||
assert!(index.contains("id=\"settings-activity-identity-protection-on\" value=\"on\" checked"));
|
||||
assert!(settings.contains("set_activity_identity_protection"));
|
||||
assert!(settings.contains("adoptActivityIdentityProtectionFromBackend"));
|
||||
assert!(activity.contains("function activityRevealEvent(event)"));
|
||||
assert!(activity.contains("activityIdentityProtectionEnabled = true"));
|
||||
assert!(!activity.contains("activityRevealField(event, 'destination');"));
|
||||
assert!(interfaces.contains("pub async fn set_activity_identity_protection"));
|
||||
assert!(interfaces.contains("\"activity_identity_protection\""));
|
||||
assert!(interfaces.contains(".unwrap_or((0, false, \"auto\".to_string(), false, true, 100))"));
|
||||
assert!(tauri_lib.contains("set_activity_identity_protection"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_scale_presets_are_durable_and_backend_validated() {
|
||||
let root = repo_root();
|
||||
let settings = read_source(root.join("dashboard/static/js/settings.js")).expect("settings js");
|
||||
let scale = read_source(root.join("dashboard/static/js/text_scale.js")).expect("scale js");
|
||||
let interfaces = read_source(root.join("crates/ratspeak-tauri/src/commands/interfaces.rs"))
|
||||
.expect("interfaces commands");
|
||||
let tauri_lib = read_source(root.join("src-tauri/src/lib.rs")).expect("tauri lib");
|
||||
|
||||
assert!(settings.contains("RS.invoke('set_text_scale'"));
|
||||
assert!(settings.contains("data.text_scale_percent"));
|
||||
assert!(scale.contains("var MAX = 140"));
|
||||
assert!(interfaces.contains("pub async fn set_text_scale"));
|
||||
assert!(interfaces.contains("\"text_scale_percent\""));
|
||||
assert!(interfaces.contains("(percent.clamp(100, 140) + 5) / 10 * 10"));
|
||||
assert!(tauri_lib.contains("set_text_scale"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mobile_shells_advertise_only_portrait_orientations() {
|
||||
let root = repo_root();
|
||||
let manifest = read_source(root.join("src-tauri/gen/android/app/src/main/AndroidManifest.xml"))
|
||||
.expect("android manifest");
|
||||
let ios_info = read_source(root.join("src-tauri/gen/apple/ratspeak_iOS/Info.plist"))
|
||||
.expect("iOS Info.plist");
|
||||
let ios_project =
|
||||
read_source(root.join("src-tauri/gen/apple/project.yml")).expect("iOS project source");
|
||||
|
||||
assert!(manifest.contains("android:screenOrientation=\"sensorPortrait\""));
|
||||
assert!(manifest.contains("tools:ignore=\"DiscouragedApi,LockedOrientationActivity\""));
|
||||
assert!(ios_info.contains("UIInterfaceOrientationPortrait"));
|
||||
assert!(!ios_info.contains("UIInterfaceOrientationLandscape"));
|
||||
assert!(ios_project.contains("UISupportedInterfaceOrientations:"));
|
||||
assert!(!ios_project.contains("UIInterfaceOrientationLandscape"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ble_rnode_runtime_spawns_enable_flow_control() {
|
||||
let root = repo_root();
|
||||
|
|
|
|||
|
|
@ -1205,14 +1205,47 @@
|
|||
</div>
|
||||
<div class="settings-row settings-text-scale-row">
|
||||
<div class="settings-row-info">
|
||||
<label class="settings-row-label" for="settings-text-scale">Text size</label>
|
||||
<span class="settings-row-desc" id="settings-text-scale-desc">Scale text without changing controls or touch targets</span>
|
||||
</div>
|
||||
<div class="settings-text-scale-controls">
|
||||
<input id="settings-text-scale" class="settings-text-scale-input" type="range" min="100" max="200" step="10" value="100" aria-describedby="settings-text-scale-desc">
|
||||
<output id="settings-text-scale-value" class="settings-text-scale-value" for="settings-text-scale">100%</output>
|
||||
<button id="settings-text-scale-reset" class="settings-text-scale-reset" type="button">Reset</button>
|
||||
<span class="settings-row-label" id="settings-text-scale-label">Text size</span>
|
||||
<span class="settings-row-desc" id="settings-text-scale-desc">Choose a comfortable reading size without enlarging controls or touch targets.</span>
|
||||
</div>
|
||||
<fieldset class="settings-type-presets" aria-labelledby="settings-text-scale-label" aria-describedby="settings-text-scale-desc">
|
||||
<legend class="sr-only">Text size</legend>
|
||||
<label class="settings-type-preset" data-scale="100">
|
||||
<input type="radio" name="settings-text-scale" value="100" checked>
|
||||
<span class="settings-type-preset-card">
|
||||
<span class="settings-type-preset-sample" aria-hidden="true">Aa</span>
|
||||
<span class="settings-type-preset-value">100%</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="settings-type-preset" data-scale="110">
|
||||
<input type="radio" name="settings-text-scale" value="110">
|
||||
<span class="settings-type-preset-card">
|
||||
<span class="settings-type-preset-sample" aria-hidden="true">Aa</span>
|
||||
<span class="settings-type-preset-value">110%</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="settings-type-preset" data-scale="120">
|
||||
<input type="radio" name="settings-text-scale" value="120">
|
||||
<span class="settings-type-preset-card">
|
||||
<span class="settings-type-preset-sample" aria-hidden="true">Aa</span>
|
||||
<span class="settings-type-preset-value">120%</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="settings-type-preset" data-scale="130">
|
||||
<input type="radio" name="settings-text-scale" value="130">
|
||||
<span class="settings-type-preset-card">
|
||||
<span class="settings-type-preset-sample" aria-hidden="true">Aa</span>
|
||||
<span class="settings-type-preset-value">130%</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="settings-type-preset" data-scale="140">
|
||||
<input type="radio" name="settings-text-scale" value="140">
|
||||
<span class="settings-type-preset-card">
|
||||
<span class="settings-type-preset-sample" aria-hidden="true">Aa</span>
|
||||
<span class="settings-type-preset-value">140%</span>
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<div class="settings-row-info">
|
||||
|
|
@ -1255,11 +1288,11 @@
|
|||
<div class="settings-radio-group" role="radiogroup" aria-label="Channel hosting">
|
||||
<label class="settings-radio-option">
|
||||
<input type="radio" name="settings-channel-hosting" id="settings-channel-hosting-off" value="off" checked>
|
||||
<span>Off</span>
|
||||
<span>OFF</span>
|
||||
</label>
|
||||
<label class="settings-radio-option">
|
||||
<input type="radio" name="settings-channel-hosting" id="settings-channel-hosting-on" value="on">
|
||||
<span>On</span>
|
||||
<span>ON</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1313,6 +1346,22 @@
|
|||
<div class="panel settings-panel" id="panel-settings-privacy">
|
||||
<div class="panel-header">Privacy</div>
|
||||
<div class="panel-body">
|
||||
<div class="settings-row">
|
||||
<div class="settings-row-info">
|
||||
<span class="settings-row-label">Protect Activity identities</span>
|
||||
<span class="settings-row-desc" id="settings-activity-identity-protection-desc" aria-live="polite">Hide peer names and addresses until you reveal an Activity event.</span>
|
||||
</div>
|
||||
<div class="settings-radio-group" role="radiogroup" aria-label="Protect Activity identities">
|
||||
<label class="settings-radio-option">
|
||||
<input type="radio" name="settings-activity-identity-protection" id="settings-activity-identity-protection-off" value="off">
|
||||
<span>OFF</span>
|
||||
</label>
|
||||
<label class="settings-radio-option">
|
||||
<input type="radio" name="settings-activity-identity-protection" id="settings-activity-identity-protection-on" value="on" checked>
|
||||
<span>ON</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-row" style="border-bottom:none;">
|
||||
<div class="settings-row-info">
|
||||
<span class="settings-row-label">Announce Ratspeak usage</span>
|
||||
|
|
@ -1375,11 +1424,11 @@
|
|||
<div class="settings-radio-group" role="radiogroup" aria-label="Developer Mode">
|
||||
<label class="settings-radio-option">
|
||||
<input type="radio" name="settings-developer-mode" id="settings-developer-mode-off" value="off" checked>
|
||||
<span>Off</span>
|
||||
<span>OFF</span>
|
||||
</label>
|
||||
<label class="settings-radio-option">
|
||||
<input type="radio" name="settings-developer-mode" id="settings-developer-mode-on" value="on">
|
||||
<span>On</span>
|
||||
<span>ON</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1391,15 +1440,15 @@
|
|||
<div class="settings-radio-group" role="radiogroup" aria-label="Window Decorations">
|
||||
<label class="settings-radio-option">
|
||||
<input type="radio" name="settings-window-decorations" id="settings-window-decorations-auto" value="auto" checked>
|
||||
<span>Auto</span>
|
||||
<span>AUTO</span>
|
||||
</label>
|
||||
<label class="settings-radio-option">
|
||||
<input type="radio" name="settings-window-decorations" id="settings-window-decorations-on" value="on">
|
||||
<span>On</span>
|
||||
<span>ON</span>
|
||||
</label>
|
||||
<label class="settings-radio-option">
|
||||
<input type="radio" name="settings-window-decorations" id="settings-window-decorations-off" value="off">
|
||||
<span>Off</span>
|
||||
<span>OFF</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1150,6 +1150,7 @@ test('identifier controls reveal first and copy only after disclosure', async fu
|
|||
assert(hiddenHtml.indexOf('<code>Reveal</code>') !== -1);
|
||||
assert(hiddenHtml.indexOf('data-action="reveal"') !== -1);
|
||||
assert(hiddenHtml.indexOf('data-icon="reveal"') !== -1);
|
||||
assert(hiddenHtml.indexOf('Alice') === -1, 'peer names stay private before explicit reveal');
|
||||
|
||||
var copied = null;
|
||||
ctx.RS.copyText = function(value) {
|
||||
|
|
@ -1176,6 +1177,96 @@ test('identifier controls reveal first and copy only after disclosure', async fu
|
|||
assert.strictEqual(revealCalls, 1, 'copy should reuse the prior explicit reveal');
|
||||
});
|
||||
|
||||
test('one Activity disclosure reveals every identifier in only that event', async function() {
|
||||
var ui = loadUiHarness();
|
||||
var ctx = ui.context;
|
||||
var destination = '00112233445566778899aabbccddeeff';
|
||||
var identity = 'ffeeddccbbaa99887766554433221100';
|
||||
var peerEvent = event(SESSION_A, '10', 'rns.link.identified');
|
||||
peerEvent.area = 'links';
|
||||
peerEvent.kind = 'rns.link.identified';
|
||||
peerEvent.summary_code = 'rns.link.identified';
|
||||
peerEvent.attributes = [{
|
||||
key: 'destination',
|
||||
value: {
|
||||
type: 'identifier',
|
||||
value: { kind: 'destination', pseudonym: 'masked-destination', ordinal: 1 }
|
||||
}
|
||||
}, {
|
||||
key: 'identity',
|
||||
value: {
|
||||
type: 'identifier',
|
||||
value: { kind: 'peer', pseudonym: 'masked-identity', ordinal: 1 }
|
||||
}
|
||||
}];
|
||||
ctx.activityEvents = [peerEvent];
|
||||
ctx.activityExpandedSequence = '10';
|
||||
var fields = [];
|
||||
ui.setInvoke(function(name, args) {
|
||||
assert.strictEqual(name, 'activity_reveal');
|
||||
fields.push(args.args.field);
|
||||
var value = args.args.field === 'destination' ? destination : identity;
|
||||
return Promise.resolve({
|
||||
result: 'identifier',
|
||||
key: args.args.field,
|
||||
kind: args.args.field,
|
||||
value: value
|
||||
});
|
||||
});
|
||||
|
||||
ctx.renderActivityFeed();
|
||||
assert.strictEqual((ui.ids['activity-feed'].innerHTML.match(/data-action="reveal"/g) || []).length, 2);
|
||||
assert.strictEqual(await ctx.activateActivityIdentifier('10', 'destination'), true);
|
||||
await flush();
|
||||
ctx.renderActivityFeed();
|
||||
assert.deepStrictEqual(fields.sort(), ['destination', 'identity']);
|
||||
assert.strictEqual((ui.ids['activity-feed'].innerHTML.match(/data-action="copy"/g) || []).length, 2);
|
||||
assert(ui.ids['activity-feed'].innerHTML.indexOf('001122334…eeff') !== -1);
|
||||
assert(ui.ids['activity-feed'].innerHTML.indexOf('ffeeddccb…1100') !== -1);
|
||||
|
||||
ctx.setActivityIdentityProtectionEnabled(false);
|
||||
ctx.setActivityIdentityProtectionEnabled(true);
|
||||
ctx.renderActivityFeed();
|
||||
assert.strictEqual((ui.ids['activity-feed'].innerHTML.match(/data-action="reveal"/g) || []).length, 2,
|
||||
'turning protection on clears every transient disclosure');
|
||||
});
|
||||
|
||||
test('disabling Activity identity protection removes reveal friction without changing capture data', async function() {
|
||||
var ui = loadUiHarness();
|
||||
var ctx = ui.context;
|
||||
var raw = '1234567890abcdef1234567890abcdef';
|
||||
var peerEvent = event(SESSION_A, '11', 'lxmf.delivery.queued');
|
||||
peerEvent.area = 'messages';
|
||||
peerEvent.kind = 'lxmf.delivery.queued';
|
||||
peerEvent.summary_code = 'lxmf.delivery.queued';
|
||||
peerEvent.attributes = [{
|
||||
key: 'destination',
|
||||
value: {
|
||||
type: 'identifier',
|
||||
value: { kind: 'destination', pseudonym: 'masked-lxmf-address', ordinal: 3 }
|
||||
}
|
||||
}];
|
||||
ctx.activityEvents = [peerEvent];
|
||||
ctx.activityExpandedSequence = '11';
|
||||
ui.setInvoke(function(name, args) {
|
||||
assert.strictEqual(name, 'activity_reveal');
|
||||
return Promise.resolve({
|
||||
result: 'identifier',
|
||||
key: args.args.field,
|
||||
kind: 'destination',
|
||||
value: raw
|
||||
});
|
||||
});
|
||||
|
||||
ctx.setActivityIdentityProtectionEnabled(false);
|
||||
await flush();
|
||||
ctx.renderActivityFeed();
|
||||
assert(ui.ids['activity-feed'].innerHTML.indexOf('data-action="copy"') !== -1);
|
||||
assert(ui.ids['activity-feed'].innerHTML.indexOf('data-action="reveal"') === -1);
|
||||
assert(ui.ids['activity-feed'].innerHTML.indexOf(raw) === -1,
|
||||
'even unprotected mode keeps full addresses out of the DOM');
|
||||
});
|
||||
|
||||
test('identity UI reset clears canonical visible state without issuing a Stop command', async function() {
|
||||
var ui = loadUiHarness();
|
||||
var ctx = ui.context;
|
||||
|
|
|
|||
|
|
@ -98,7 +98,10 @@ assert(settingsFieldRule && /min-height:\s*34px/.test(settingsFieldRule[1]) && /
|
|||
'settings fields must grow with scaled text');
|
||||
assert(/data-text-scale-tier="xlarge"[\s\S]*?\.channels-layout/.test(sources[14].text),
|
||||
'the largest text tier must simplify the Channels layout');
|
||||
assert(/data-text-scale-tier="xlarge"[\s\S]*?\.settings-text-scale-input/.test(sources[14].text),
|
||||
'the largest mobile text tier must give the slider its own row');
|
||||
assert(/\.settings-type-presets\s*\{[\s\S]*?repeat\(5,/.test(sources[11].text),
|
||||
'text sizing must expose five deliberate presets instead of a continuous slider');
|
||||
assert(/data-scale="100"[\s\S]*?--type-preview-size:\s*18px/.test(sources[11].text) &&
|
||||
/data-scale="140"[\s\S]*?--type-preview-size:\s*34px/.test(sources[11].text),
|
||||
'text-size specimens must make the preset progression visually distinct');
|
||||
|
||||
console.log('CSS contract tests passed');
|
||||
|
|
|
|||
|
|
@ -67,11 +67,11 @@ scaleContext.window.localStorage = scaleContext.localStorage;
|
|||
vm.createContext(scaleContext);
|
||||
vm.runInContext(read('static/js/text_scale.js'), scaleContext);
|
||||
assert.strictEqual(scaleContext.window.RS.textScale.get(), 100);
|
||||
assert.strictEqual(scaleContext.window.RS.textScale.commit(147), 150);
|
||||
assert.strictEqual(root.style.fontSize, '150%');
|
||||
assert.strictEqual(scaleContext.window.RS.textScale.commit(127), 130);
|
||||
assert.strictEqual(root.style.fontSize, '130%');
|
||||
assert.strictEqual(root.dataset.textScaleTier, 'large');
|
||||
assert.strictEqual(storage['rs-text-scale-percent'], '150');
|
||||
assert.strictEqual(scaleContext.window.RS.textScale.commit(200), 200);
|
||||
assert.strictEqual(storage['rs-text-scale-percent'], '130');
|
||||
assert.strictEqual(scaleContext.window.RS.textScale.commit(200), 140);
|
||||
assert.strictEqual(root.dataset.textScaleTier, 'xlarge');
|
||||
assert.strictEqual(scaleContext.window.RS.textScale.reset(), 100);
|
||||
assert.strictEqual(storage['rs-text-scale-percent'], undefined);
|
||||
|
|
@ -95,10 +95,13 @@ var labelTargets = Array.from(html.matchAll(/<label\b[^>]*\sfor="([^"]+)"/g), fu
|
|||
assert.deepStrictEqual(Array.from(new Set(labelTargets.filter(function(id) {
|
||||
return ids.indexOf(id) === -1;
|
||||
}))), [], 'label for attributes must resolve to dashboard controls');
|
||||
assert(html.includes('id="settings-text-scale"'));
|
||||
assert(html.includes('min="100" max="200" step="10"'));
|
||||
assert.strictEqual((html.match(/name="settings-text-scale"/g) || []).length, 5);
|
||||
['100', '110', '120', '130', '140'].forEach(function(value) {
|
||||
assert(html.includes('name="settings-text-scale" value="' + value + '"'));
|
||||
});
|
||||
assert(html.includes('class="settings-type-presets"'));
|
||||
assert(html.includes('aria-labelledby="settings-text-scale-label"'));
|
||||
assert(html.includes('aria-describedby="settings-text-scale-desc"'));
|
||||
assert(!html.includes('aria-describedby="settings-text-scale-desc settings-text-scale-value"'));
|
||||
assert(!html.includes('no_pinch.js'), 'browser zoom must remain available');
|
||||
assert(html.includes('lxmf-compose message-composer'));
|
||||
assert(html.includes('channel-compose message-composer'));
|
||||
|
|
|
|||
|
|
@ -1817,6 +1817,13 @@
|
|||
padding: var(--space-1) var(--space-1) var(--space-3);
|
||||
border-bottom: 1px solid var(--border-row);
|
||||
}
|
||||
.settings-detail-header > div,
|
||||
.settings-row-info {
|
||||
min-width: 0;
|
||||
}
|
||||
.settings-detail-header > div {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.settings-detail-eyebrow {
|
||||
display: block;
|
||||
color: var(--text-secondary);
|
||||
|
|
@ -1832,12 +1839,14 @@
|
|||
line-height: 1.15;
|
||||
}
|
||||
.settings-detail-header p {
|
||||
flex: 0 1 360px;
|
||||
max-width: 360px;
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-base);
|
||||
line-height: var(--type-leading-body);
|
||||
text-align: right;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.settings-page-inner {
|
||||
max-width: none;
|
||||
|
|
@ -1871,57 +1880,84 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settings-text-scale-controls {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(140px, 210px) 3.5rem auto;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
gap: var(--space-4);
|
||||
.settings-text-scale-row {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
.settings-text-scale-row .settings-row-info {
|
||||
max-width: none;
|
||||
}
|
||||
.settings-type-presets {
|
||||
width: min(100%, 680px);
|
||||
min-width: 0;
|
||||
}
|
||||
.settings-text-scale-input {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
margin: 0;
|
||||
accent-color: var(--accent);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(64px, 112px));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
.settings-type-preset {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.settings-text-scale-input:focus-visible {
|
||||
outline: 2px solid var(--focus-ring);
|
||||
outline-offset: 3px;
|
||||
border-radius: var(--radius-sm);
|
||||
/* The specimen is a choice cue, not a literal rendering sample. Use a wider
|
||||
progression than the applied percentages so adjacent presets scan clearly. */
|
||||
.settings-type-preset[data-scale="100"] { --type-preview-size: 18px; }
|
||||
.settings-type-preset[data-scale="110"] { --type-preview-size: 22px; }
|
||||
.settings-type-preset[data-scale="120"] { --type-preview-size: 26px; }
|
||||
.settings-type-preset[data-scale="130"] { --type-preview-size: 30px; }
|
||||
.settings-type-preset[data-scale="140"] { --type-preview-size: 34px; }
|
||||
.settings-type-preset input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.settings-text-scale-value {
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
}
|
||||
.settings-text-scale-reset {
|
||||
min-height: 44px;
|
||||
padding: 0 var(--space-5);
|
||||
.settings-type-preset-card {
|
||||
min-height: 76px;
|
||||
padding: var(--space-4) var(--space-3);
|
||||
border: 1px solid var(--border-control);
|
||||
border-radius: var(--radius-md);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--surface-control);
|
||||
color: var(--text-secondary);
|
||||
font: inherit;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--type-weight-semibold);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast), color var(--transition-fast), opacity var(--transition-fast);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-3);
|
||||
transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
.settings-text-scale-reset:hover:not(:disabled) {
|
||||
.settings-type-preset:hover .settings-type-preset-card {
|
||||
background: var(--surface-control-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.settings-text-scale-reset:focus-visible {
|
||||
.settings-type-preset input:checked + .settings-type-preset-card {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-bg-strong);
|
||||
color: var(--accent);
|
||||
box-shadow: inset 0 0 0 1px var(--accent-border);
|
||||
}
|
||||
.settings-type-preset input:focus-visible + .settings-type-preset-card {
|
||||
outline: 2px solid var(--focus-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.settings-text-scale-reset:disabled {
|
||||
opacity: var(--opacity-disabled);
|
||||
cursor: default;
|
||||
.settings-type-preset-sample {
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--type-preview-size);
|
||||
font-weight: var(--type-weight-semibold);
|
||||
line-height: 1;
|
||||
}
|
||||
.settings-type-preset-value {
|
||||
max-width: 100%;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--type-badge-size);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
html[data-text-scale-tier="large"] .settings-row {
|
||||
|
|
@ -1932,10 +1968,15 @@ html[data-text-scale-tier="large"] .settings-row-info {
|
|||
min-width: min(100%, 250px);
|
||||
flex: 1 1 250px;
|
||||
}
|
||||
html[data-text-scale-tier="large"] .settings-row-actions,
|
||||
html[data-text-scale-tier="large"] .settings-text-scale-controls {
|
||||
html[data-text-scale-tier="large"] .settings-row-actions {
|
||||
flex: 1 1 280px;
|
||||
}
|
||||
html[data-text-scale-tier="large"] .settings-detail-header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
html[data-text-scale-tier="large"] .settings-detail-header p {
|
||||
max-width: 48%;
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
html[data-text-scale-tier="xlarge"] .settings-page-shell {
|
||||
|
|
@ -1956,6 +1997,7 @@ html[data-text-scale-tier="large"] .settings-text-scale-controls {
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
html[data-text-scale-tier="xlarge"] .settings-detail-header p {
|
||||
flex-basis: 100%;
|
||||
max-width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1154,26 +1154,14 @@
|
|||
max-width: 48%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.settings-text-scale-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
.settings-text-scale-controls {
|
||||
.settings-type-presets {
|
||||
width: 100%;
|
||||
grid-template-columns: minmax(0, 1fr) 3.75rem auto;
|
||||
justify-content: stretch;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: var(--space-2);
|
||||
}
|
||||
html[data-text-scale-tier="xlarge"] .settings-text-scale-controls {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
html[data-text-scale-tier="xlarge"] .settings-text-scale-input {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
html[data-text-scale-tier="xlarge"] .settings-text-scale-value {
|
||||
grid-column: 1;
|
||||
justify-self: start;
|
||||
}
|
||||
html[data-text-scale-tier="xlarge"] .settings-text-scale-reset {
|
||||
grid-column: 2;
|
||||
.settings-type-preset-card {
|
||||
min-height: 72px;
|
||||
padding-inline: var(--space-2);
|
||||
}
|
||||
.settings-row-label {
|
||||
font-size: 1rem;
|
||||
|
|
@ -3485,6 +3473,10 @@ html[data-text-scale-tier="large"] .dashboard-action-row {
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
html[data-text-scale-tier="large"] .activity-event-details > div {
|
||||
grid-template-columns: minmax(96px, 0.28fr) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
html[data-text-scale-tier="xlarge"] .lxmf-sidebar {
|
||||
width: clamp(320px, 34vw, 440px);
|
||||
|
|
@ -3502,6 +3494,11 @@ html[data-text-scale-tier="xlarge"] .bento-grid-dashboard {
|
|||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
html[data-text-scale-tier="xlarge"] .activity-event-details > div {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
html[data-text-scale-tier="xlarge"] .channels-layout {
|
||||
grid-template-columns: minmax(272px, 34vw) minmax(360px, 1fr);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ var activitySearchQuery = '';
|
|||
var activityExpandedSequence = null;
|
||||
var activityRevealedFields = Object.create(null);
|
||||
var activityRevealPending = Object.create(null);
|
||||
var activityRevealEventPending = Object.create(null);
|
||||
var activityIdentityProtectionEnabled = true;
|
||||
|
||||
var ACTIVITY_MAX_RENDERED = 500;
|
||||
var _activityRenderScheduled = false;
|
||||
|
|
@ -1177,12 +1179,6 @@ function toggleActivityEvent(sequence) {
|
|||
activityExpandedSequence = expanding ? sequence : null;
|
||||
activityStickToBottom = false;
|
||||
renderActivityFeed();
|
||||
if (expanding) {
|
||||
var event = activityEventBySequence(sequence);
|
||||
if (event && event.kind === 'rns.announce.observed') {
|
||||
activityRevealField(event, 'destination');
|
||||
}
|
||||
}
|
||||
var row = document.querySelector('[data-activity-sequence="' + sequence + '"]');
|
||||
var toggle = row && row.querySelector ? row.querySelector('.activity-event-toggle') : null;
|
||||
if (toggle && typeof toggle.focus === 'function') {
|
||||
|
|
@ -1272,12 +1268,35 @@ function activityPresentationArea(event) {
|
|||
function activityResetReveals() {
|
||||
activityRevealedFields = Object.create(null);
|
||||
activityRevealPending = Object.create(null);
|
||||
activityRevealEventPending = Object.create(null);
|
||||
}
|
||||
|
||||
function activityRevealKey(event, field) {
|
||||
return event.capture_session + ':' + event.sequence + ':' + field;
|
||||
}
|
||||
|
||||
function activityEventRevealKey(event) {
|
||||
return event.capture_session + ':' + event.sequence;
|
||||
}
|
||||
|
||||
function activityIdentifierAttributeIsRevealable(attribute) {
|
||||
return !!(attribute && attribute.value && attribute.value.type === 'identifier') &&
|
||||
['destination', 'endpoint', 'hub', 'identity', 'link', 'message', 'room'].indexOf(attribute.key) !== -1;
|
||||
}
|
||||
|
||||
function activityIdentifierAttributes(event) {
|
||||
return (event && Array.isArray(event.attributes) ? event.attributes : []).filter(
|
||||
activityIdentifierAttributeIsRevealable
|
||||
);
|
||||
}
|
||||
|
||||
function activityEventIdentifiersRevealed(event) {
|
||||
var identifiers = activityIdentifierAttributes(event);
|
||||
return identifiers.length === 0 || identifiers.every(function(attribute) {
|
||||
return !!activityRevealedField(event, attribute.key);
|
||||
});
|
||||
}
|
||||
|
||||
function activityRevealedField(event, field) {
|
||||
var key = activityRevealKey(event, field);
|
||||
return Object.prototype.hasOwnProperty.call(activityRevealedFields, key)
|
||||
|
|
@ -1328,6 +1347,47 @@ function activityRevealField(event, field) {
|
|||
return activityRevealPending[key];
|
||||
}
|
||||
|
||||
function activityRevealEvent(event) {
|
||||
if (!event || !activityIsCanonicalU64(event.sequence, false) || !event.capture_session) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
var identifiers = activityIdentifierAttributes(event);
|
||||
if (!identifiers.length) return Promise.resolve(false);
|
||||
if (activityEventIdentifiersRevealed(event)) return Promise.resolve(true);
|
||||
var key = activityEventRevealKey(event);
|
||||
if (activityRevealEventPending[key]) return activityRevealEventPending[key];
|
||||
activityRevealEventPending[key] = Promise.resolve().then(function() {
|
||||
return Promise.all(identifiers.map(function(attribute) {
|
||||
return activityRevealField(event, attribute.key);
|
||||
}));
|
||||
}).then(function(values) {
|
||||
delete activityRevealEventPending[key];
|
||||
scheduleActivityRender();
|
||||
return values.every(function(value) { return !!value; });
|
||||
}, function() {
|
||||
delete activityRevealEventPending[key];
|
||||
scheduleActivityRender();
|
||||
return false;
|
||||
});
|
||||
scheduleActivityRender();
|
||||
return activityRevealEventPending[key];
|
||||
}
|
||||
|
||||
function setActivityIdentityProtectionEnabled(enabled) {
|
||||
var next = enabled !== false;
|
||||
if (next === activityIdentityProtectionEnabled) return;
|
||||
activityIdentityProtectionEnabled = next;
|
||||
if (next) activityResetReveals();
|
||||
scheduleActivityRender();
|
||||
}
|
||||
|
||||
if (typeof RS !== 'undefined') {
|
||||
RS.activityIdentityProtection = {
|
||||
get: function() { return activityIdentityProtectionEnabled; },
|
||||
set: setActivityIdentityProtectionEnabled
|
||||
};
|
||||
}
|
||||
|
||||
function activityKnownPeerForHash(hash) {
|
||||
if (!hash) return null;
|
||||
if (typeof PeersCache !== 'undefined' && PeersCache && typeof PeersCache.get === 'function') {
|
||||
|
|
@ -1388,6 +1448,7 @@ function activityIdentifierLabel(event, field) {
|
|||
if (field === 'destination' && announce && announce.isLxmf) return 'LXMF address';
|
||||
var labels = {
|
||||
destination: 'Destination',
|
||||
endpoint: 'Endpoint',
|
||||
hub: 'Hub address',
|
||||
identity: 'Identity hash',
|
||||
link: 'Link identifier',
|
||||
|
|
@ -1415,7 +1476,8 @@ function activityRevealIcon() {
|
|||
function activityIdentifierControl(event, attribute) {
|
||||
var field = attribute.key;
|
||||
var raw = activityRevealedField(event, field);
|
||||
var pending = !!activityRevealPending[activityRevealKey(event, field)];
|
||||
var pending = !!activityRevealEventPending[activityEventRevealKey(event)] ||
|
||||
!!activityRevealPending[activityRevealKey(event, field)];
|
||||
var label = activityIdentifierLabel(event, field);
|
||||
var action = raw ? 'copy' : 'reveal';
|
||||
var text = raw ? activityShortToken(raw) : (pending ? 'Revealing…' : 'Reveal');
|
||||
|
|
@ -1434,12 +1496,11 @@ function activateActivityIdentifier(sequence, field) {
|
|||
if (activityRevealedField(event, field)) {
|
||||
return copyActivityIdentifier(sequence, field);
|
||||
}
|
||||
var label = activityIdentifierLabel(event, field);
|
||||
return activityRevealField(event, field).then(function(value) {
|
||||
if (!value && typeof showToast === 'function') {
|
||||
showToast('Could not reveal ' + label.toLowerCase(), 'toast-red', 3000);
|
||||
return activityRevealEvent(event).then(function(revealed) {
|
||||
if (!revealed && typeof showToast === 'function') {
|
||||
showToast('Could not reveal this event’s identities', 'toast-red', 3000);
|
||||
}
|
||||
return !!value;
|
||||
return !!revealed;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1718,7 +1779,7 @@ function activityEventDetails(event) {
|
|||
]);
|
||||
}
|
||||
(event.attributes || []).forEach(function(attribute) {
|
||||
if (attribute.value && attribute.value.type === 'identifier') {
|
||||
if (activityIdentifierAttributeIsRevealable(attribute)) {
|
||||
rows.push([
|
||||
activityIdentifierLabel(event, attribute.key),
|
||||
activityIdentifierControl(event, attribute)
|
||||
|
|
@ -1778,6 +1839,9 @@ function renderActivityFeed() {
|
|||
for (var i = 0; i < visible.length; i++) {
|
||||
var event = visible[i];
|
||||
var expanded = event.sequence === activityExpandedSequence;
|
||||
if (expanded && !activityIdentityProtectionEnabled && !activityEventIdentifiersRevealed(event)) {
|
||||
activityRevealEvent(event);
|
||||
}
|
||||
var problem = activityIsProblem(event);
|
||||
var outcome = event.outcome && event.outcome !== 'none' ? event.outcome : '';
|
||||
var presentationArea = activityPresentationArea(event);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ function openSettings() {
|
|||
showSettingsMobileSectionIndex({ restoreFocus: false });
|
||||
initHapticsToggle();
|
||||
initChannelHostingToggle();
|
||||
initActivityIdentityProtectionToggle();
|
||||
initDeveloperModeToggle();
|
||||
initWindowDecorationsToggle();
|
||||
syncSettingsIdentityActions();
|
||||
|
|
@ -25,6 +26,9 @@ var _settingsChannelHostingBusy = false;
|
|||
var _settingsChannelHostingEnabled = false;
|
||||
var _settingsChannelHostingRequested = null;
|
||||
var _settingsChannelHostingSupported = null;
|
||||
var _settingsActivityIdentityProtectionBound = false;
|
||||
var _settingsActivityIdentityProtectionBusy = false;
|
||||
var _settingsActivityIdentityProtectionEnabled = true;
|
||||
var RATSPEAK_RELEASE_LATEST_URL = 'https://api.github.com/repos/ratspeak/Ratspeak/releases/latest';
|
||||
var RATSPEAK_RELEASES_URL = 'https://github.com/ratspeak/Ratspeak/releases';
|
||||
|
||||
|
|
@ -135,6 +139,73 @@ function initChannelHostingToggle() {
|
|||
}
|
||||
}
|
||||
|
||||
function syncActivityIdentityProtectionRadioState() {
|
||||
var off = document.getElementById('settings-activity-identity-protection-off');
|
||||
var on = document.getElementById('settings-activity-identity-protection-on');
|
||||
var group = on && on.closest('.settings-radio-group');
|
||||
if (off) {
|
||||
off.checked = !_settingsActivityIdentityProtectionEnabled;
|
||||
off.disabled = _settingsActivityIdentityProtectionBusy;
|
||||
}
|
||||
if (on) {
|
||||
on.checked = _settingsActivityIdentityProtectionEnabled;
|
||||
on.disabled = _settingsActivityIdentityProtectionBusy;
|
||||
}
|
||||
if (group) {
|
||||
group.setAttribute('aria-busy', _settingsActivityIdentityProtectionBusy ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function adoptActivityIdentityProtectionFromBackend(enabled) {
|
||||
_settingsActivityIdentityProtectionEnabled = enabled !== false;
|
||||
syncActivityIdentityProtectionRadioState();
|
||||
if (RS.activityIdentityProtection && typeof RS.activityIdentityProtection.set === 'function') {
|
||||
RS.activityIdentityProtection.set(_settingsActivityIdentityProtectionEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
function setActivityIdentityProtectionEnabled(enabled) {
|
||||
if (_settingsActivityIdentityProtectionBusy) return;
|
||||
var previous = _settingsActivityIdentityProtectionEnabled;
|
||||
_settingsActivityIdentityProtectionEnabled = !!enabled;
|
||||
_settingsActivityIdentityProtectionBusy = true;
|
||||
syncActivityIdentityProtectionRadioState();
|
||||
if (RS.activityIdentityProtection && typeof RS.activityIdentityProtection.set === 'function') {
|
||||
RS.activityIdentityProtection.set(_settingsActivityIdentityProtectionEnabled);
|
||||
}
|
||||
RS.invoke('set_activity_identity_protection', { enabled: _settingsActivityIdentityProtectionEnabled })
|
||||
.then(function(result) {
|
||||
adoptActivityIdentityProtectionFromBackend(
|
||||
result && result.enabled !== undefined ? result.enabled : enabled
|
||||
);
|
||||
})
|
||||
.catch(function(error) {
|
||||
adoptActivityIdentityProtectionFromBackend(previous);
|
||||
if (typeof showToast === 'function') {
|
||||
showToast((error && error.message) || 'Could not update Activity privacy', 'toast-red', 4000);
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
_settingsActivityIdentityProtectionBusy = false;
|
||||
syncActivityIdentityProtectionRadioState();
|
||||
});
|
||||
}
|
||||
|
||||
function initActivityIdentityProtectionToggle() {
|
||||
var off = document.getElementById('settings-activity-identity-protection-off');
|
||||
var on = document.getElementById('settings-activity-identity-protection-on');
|
||||
if (!off || !on) return;
|
||||
syncActivityIdentityProtectionRadioState();
|
||||
if (_settingsActivityIdentityProtectionBound) return;
|
||||
_settingsActivityIdentityProtectionBound = true;
|
||||
off.addEventListener('change', function() {
|
||||
if (off.checked) setActivityIdentityProtectionEnabled(false);
|
||||
});
|
||||
on.addEventListener('change', function() {
|
||||
if (on.checked) setActivityIdentityProtectionEnabled(true);
|
||||
});
|
||||
}
|
||||
|
||||
function readDeveloperModePreference() {
|
||||
try {
|
||||
return window.localStorage.getItem(_settingsDeveloperModeStorageKey) === 'true';
|
||||
|
|
@ -1416,6 +1487,12 @@ function applyAppSettingsPayload(data) {
|
|||
if (usageToggle && data.announce_ratspeak_usage !== undefined) {
|
||||
usageToggle.checked = !!data.announce_ratspeak_usage;
|
||||
}
|
||||
if (data.activity_identity_protection !== undefined) {
|
||||
adoptActivityIdentityProtectionFromBackend(data.activity_identity_protection);
|
||||
}
|
||||
if (data.text_scale_percent !== undefined && RS.textScale) {
|
||||
RS.textScale.commit(data.text_scale_percent);
|
||||
}
|
||||
var hwBadge = document.getElementById('hw-lock-timeout-select');
|
||||
if (hwBadge && data.hardware_session_timeout !== undefined) {
|
||||
var t = parseInt(data.hardware_session_timeout, 10);
|
||||
|
|
@ -1489,6 +1566,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
(function() {
|
||||
var usageToggle = document.getElementById('announce-ratspeak-usage-toggle');
|
||||
initActivityIdentityProtectionToggle();
|
||||
RS.invoke('api_app_settings').then(applyAppSettingsPayload).catch(function() {});
|
||||
if (!usageToggle) return;
|
||||
usageToggle.addEventListener('change', function() {
|
||||
|
|
@ -1842,7 +1920,7 @@ function confirmDangerAction(action, onClose) {
|
|||
var _themeToggleInitialized = false;
|
||||
var _hapticsToggleInitialized = false;
|
||||
var _textScaleInitialized = false;
|
||||
var _textScalePreviewFrame = null;
|
||||
var _textScaleSaving = false;
|
||||
|
||||
function initThemeToggle() {
|
||||
var toggle = document.getElementById('theme-toggle');
|
||||
|
|
@ -1887,44 +1965,49 @@ function initHapticsToggle() {
|
|||
}
|
||||
|
||||
function initTextScaleControl() {
|
||||
var input = document.getElementById('settings-text-scale');
|
||||
var output = document.getElementById('settings-text-scale-value');
|
||||
var reset = document.getElementById('settings-text-scale-reset');
|
||||
if (!input || !output || !reset || !RS.textScale) return;
|
||||
var inputs = document.querySelectorAll('input[name="settings-text-scale"]');
|
||||
if (!inputs.length || !RS.textScale) return;
|
||||
|
||||
function sync(value) {
|
||||
var percent = RS.textScale.normalize(value);
|
||||
input.value = String(percent);
|
||||
input.setAttribute('aria-valuetext', percent + ' percent');
|
||||
output.value = percent + '%';
|
||||
output.textContent = percent + '%';
|
||||
reset.disabled = percent === RS.textScale.MIN;
|
||||
inputs.forEach(function(input) {
|
||||
var selected = Number(input.value) === percent;
|
||||
input.checked = selected;
|
||||
input.disabled = _textScaleSaving;
|
||||
});
|
||||
var fieldset = inputs[0].closest('fieldset');
|
||||
if (fieldset) fieldset.setAttribute('aria-busy', _textScaleSaving ? 'true' : 'false');
|
||||
}
|
||||
|
||||
function save(value) {
|
||||
if (_textScaleSaving) return;
|
||||
var previous = RS.textScale.get();
|
||||
var percent = RS.textScale.commit(value);
|
||||
_textScaleSaving = true;
|
||||
sync(percent);
|
||||
RS.invoke('set_text_scale', { percent: percent }).then(function(result) {
|
||||
RS.textScale.commit(result && result.percent !== undefined ? result.percent : percent);
|
||||
}).catch(function(error) {
|
||||
RS.textScale.commit(previous);
|
||||
if (typeof showToast === 'function') {
|
||||
showToast((error && error.message) || 'Could not save text size', 'toast-red', 4000);
|
||||
}
|
||||
}).then(function() {
|
||||
_textScaleSaving = false;
|
||||
sync(RS.textScale.get());
|
||||
});
|
||||
}
|
||||
|
||||
sync(RS.textScale.get());
|
||||
if (_textScaleInitialized) return;
|
||||
_textScaleInitialized = true;
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
var next = input.value;
|
||||
sync(next);
|
||||
if (_textScalePreviewFrame) cancelAnimationFrame(_textScalePreviewFrame);
|
||||
_textScalePreviewFrame = requestAnimationFrame(function() {
|
||||
_textScalePreviewFrame = null;
|
||||
RS.textScale.preview(next);
|
||||
inputs.forEach(function(input) {
|
||||
input.addEventListener('change', function() {
|
||||
if (!input.checked) return;
|
||||
save(input.value);
|
||||
});
|
||||
});
|
||||
input.addEventListener('change', function() {
|
||||
if (_textScalePreviewFrame) {
|
||||
cancelAnimationFrame(_textScalePreviewFrame);
|
||||
_textScalePreviewFrame = null;
|
||||
}
|
||||
sync(RS.textScale.commit(input.value));
|
||||
});
|
||||
reset.addEventListener('click', function() {
|
||||
sync(RS.textScale.reset());
|
||||
input.focus({ preventScroll: true });
|
||||
});
|
||||
window.addEventListener('ratspeak-text-scale-changed', function(event) {
|
||||
if (event.detail) sync(event.detail.percent);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
var STORAGE_KEY = 'rs-text-scale-percent';
|
||||
var MIN = 100;
|
||||
var MAX = 200;
|
||||
var MAX = 140;
|
||||
var STEP = 10;
|
||||
|
||||
function normalize(value) {
|
||||
|
|
@ -19,8 +19,9 @@
|
|||
}
|
||||
|
||||
function tier(percent) {
|
||||
if (percent >= 180) return 'xlarge';
|
||||
if (percent >= 140) return 'large';
|
||||
if (percent >= 140) return 'xlarge';
|
||||
if (percent >= 130) return 'large';
|
||||
if (percent >= 120) return 'medium';
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,11 +55,13 @@
|
|||
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
tools:ignore="DiscouragedApi,LockedOrientationActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
|
|
|||
|
|
@ -46,13 +46,9 @@ targets:
|
|||
UIRequiredDeviceCapabilities: [arm64, metal]
|
||||
UISupportedInterfaceOrientations:
|
||||
- UIInterfaceOrientationPortrait
|
||||
- UIInterfaceOrientationLandscapeLeft
|
||||
- UIInterfaceOrientationLandscapeRight
|
||||
UISupportedInterfaceOrientations~ipad:
|
||||
- UIInterfaceOrientationPortrait
|
||||
- UIInterfaceOrientationPortraitUpsideDown
|
||||
- UIInterfaceOrientationLandscapeLeft
|
||||
- UIInterfaceOrientationLandscapeRight
|
||||
CFBundleShortVersionString: 1.0.26
|
||||
CFBundleVersion: "1.0.26"
|
||||
entitlements:
|
||||
|
|
|
|||
|
|
@ -30,15 +30,11 @@
|
|||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>Ratspeak uses Bluetooth to connect to hardware nodes and other Bluetooth peers when enabled.</string>
|
||||
|
|
|
|||
|
|
@ -769,6 +769,8 @@ pub fn run() {
|
|||
ratspeak_tauri::commands::interfaces::set_hardware_lock_timeout,
|
||||
ratspeak_tauri::commands::interfaces::set_developer_mode,
|
||||
ratspeak_tauri::commands::interfaces::set_announce_ratspeak_usage,
|
||||
ratspeak_tauri::commands::interfaces::set_activity_identity_protection,
|
||||
ratspeak_tauri::commands::interfaces::set_text_scale,
|
||||
ratspeak_tauri::commands::interfaces::api_notification_settings,
|
||||
ratspeak_tauri::commands::interfaces::set_desktop_notifications,
|
||||
ratspeak_tauri::commands::interfaces::add_lora_interface,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue