Skip to content

Save client-side settings on the client; server-config.json for server only#59

Open
alishair7071 wants to merge 5 commits intoAOSSIE-Org:mainfrom
alishair7071:feat/client-settings-on-client-only
Open

Save client-side settings on the client; server-config.json for server only#59
alishair7071 wants to merge 5 commits intoAOSSIE-Org:mainfrom
alishair7071:feat/client-settings-on-client-only

Conversation

@alishair7071
Copy link
Contributor

@alishair7071 alishair7071 commented Feb 10, 2026

Summary

Ensures only server config is written to server-config.json. The server accepts and writes only host, frontendPort, and address; any other key from the client is ignored. Client-side settings (e.g. Server IP, theme, mouse sensitivity, invert scroll) stay on the client only and never enter server-config.json. Documents the split in code.

Fixes #55

Changes

src/server/websocket.ts

  • On update-config, only merge whitelisted server keys: host, frontendPort, address. Any other key from the client (e.g. mouseInvert, mouseSensitivity, or client-only data) is ignored so it never enters server-config.json.

src/config.tsx

  • JSDoc updated: only host, frontendPort, and address are writable via Save Config; other keys in the file may exist for reading but are not updated from the client.
  • Notes that client settings (e.g. rein_ip, rein-theme) are localStorage-only and must never be written to server-config.json.
  • Comment on CONFIG that it is from server-config.json.

src/routes/settings.tsx

  • No changes in this PR. Settings UI and Save payload are unchanged; the server simply ignores non-whitelisted keys.

Testing

  • Save Config still sends port (and optionally host/address when added to the UI). Only host, frontendPort, and address are written to server-config.json. Sending mouseInvert, mouseSensitivity, or any other key in update-config does not write them to the file. Server IP and other client settings remain client-side only.

Summary by CodeRabbit

  • New Features

    • Theme support (Light/Dark/Default) with selector; preference persisted and applied on load.
    • Mouse sensitivity and invert moved to client-side settings, saved locally and applied instantly.
    • Settings page loads values from local storage and shows the active theme.
    • Share URL/QR now respects configured frontend port; IP is persisted.
  • Bug Fixes

    • Prevented initial load from overwriting saved settings.
    • Validates frontend port before saving and shows clearer errors for connection/update failures.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

Moves UI/preferences (theme, mouse sensitivity, invert, saved IP) to client-only localStorage, initializes theme on load, updates gesture hook to read client settings at runtime, and filters WebSocket-config updates so only whitelisted server keys are persisted to server-config.json.

Changes

Cohort / File(s) Summary
Config
src/config.tsx
Formatting and comments clarifying server vs client settings; added nullish default for FRONTEND_PORT (3000) and minor literal reformatting.
Settings UI / client persistence
src/routes/settings.tsx
Moves theme, sensitivity, invert, and saved IP into localStorage; loads/persists them client-side with initial-load guards; adds theme selector and applies theme to document; save (server) flow now validates frontendPort and sends only that via WebSocket; QR/share URL uses stored frontend port; WebSocket error handling improved.
Root / theme init
src/routes/__root.tsx
Adds ThemeInit component mounted in root document to apply stored theme on initial render.
Trackpad gesture hook
src/hooks/useTrackpadGesture.ts
Signature renamed _defaultSensitivity and adds axisThreshold; runtime reads sensitivity and invert from client storage and applies sign/scaling across pinch, scroll, and move handling.
WebSocket server filtering
src/server/websocket.ts
Introduces SERVER_CONFIG_KEYS whitelist and filters incoming msg.config so only server-designated keys (host, frontendPort, address) are merged and persisted; retains existing write/notify flow and error handling.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant WS_Server as "WebSocket\nServer"
    participant ServerFS as "server-config.json\n(File System)"

    Client->>WS_Server: send update-config (payload: { frontendPort })
    WS_Server->>WS_Server: filter msg.config using SERVER_CONFIG_KEYS
    alt server keys present
        WS_Server->>ServerFS: write filtered server-config.json
        ServerFS-->>WS_Server: write OK
        WS_Server->>Client: notify config-updated
    else no server keys
        WS_Server->>Client: acknowledge (no server changes)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇 I nibbled through settings, soft and neat,
Hid themes and sensitivity beneath my seat,
Left ports for the server, safe in a file,
Client prefs hop home and stay a while,
🥕 Hooray — small changes, big tidy feat!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective: separating client-side settings storage from server-config.json for server settings only.
Linked Issues check ✅ Passed All coding requirements from issue #55 are met: server whitelist (host, frontendPort, address) implemented, client settings isolated, and documentation added.
Out of Scope Changes check ✅ Passed All changes align with the linked issue scope: config separation, WebSocket filtering, client settings management, and theme initialization.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/routes/settings.tsx (1)

171-198: ⚠️ Potential issue | 🟠 Major

parseInt on a non-numeric port string sends NaN (serialized as null) to the server, corrupting config.

If the user types a non-numeric value in the Port field, parseInt("abc", 10) returns NaN. JSON.stringify({ frontendPort: NaN }) serializes it as null, which would corrupt server-config.json. The subsequent redirect to :NaN/settings would also fail.

Add client-side validation before sending:

Proposed fix
                     socket.onopen = () => {
                             // Only server settings; client settings (like ip) stay in localStorage
+                        const parsedPort = parseInt(frontendPort, 10);
+                        if (isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
+                            alert('Please enter a valid port number (1–65535).');
+                            socket.close();
+                            return;
+                        }
                         socket.send(JSON.stringify({
                             type: 'update-config',
                             config: {
-                                frontendPort: parseInt(frontendPort, 10),
+                                frontendPort: parsedPort,
                                 mouseInvert: invertScroll,
                                 mouseSensitivity: sensitivity,
                             }
                         }));
🧹 Nitpick comments (1)
src/server/websocket.ts (1)

57-70: Consider hoisting SERVER_CONFIG_KEYS to module scope and adding basic value validation.

Two observations:

  1. SERVER_CONFIG_KEYS is re-declared on every update-config message. Moving it to module scope (next to the imports) avoids re-creation and makes the whitelist easier to find and maintain.

  2. The key filtering is good, but there's no validation of the values. A client could send { frontendPort: "not-a-number", mouseSensitivity: null }, and these would be written directly into server-config.json. Basic type checks (e.g., typeof filtered.frontendPort === 'number') would guard against corrupt config files.

Proposed refactor

Move the constant to module scope:

+// Only these keys may be written to server-config.json
+const SERVER_CONFIG_KEYS = ['host', 'frontendPort', 'address', 'mouseInvert', 'mouseSensitivity'] as const;
+
 export function createWsServer(server: Server) {

Add lightweight value validation inside the handler:

                         const filtered: Record<string, unknown> = {};
                         for (const key of SERVER_CONFIG_KEYS) {
                             if (Object.prototype.hasOwnProperty.call(incoming, key)) {
                                 filtered[key] = incoming[key];
                             }
                         }
+                        // Basic type guards
+                        if ('frontendPort' in filtered && typeof filtered.frontendPort !== 'number') {
+                            delete filtered.frontendPort;
+                        }
+                        if ('mouseSensitivity' in filtered && typeof filtered.mouseSensitivity !== 'number') {
+                            delete filtered.mouseSensitivity;
+                        }
+                        if ('mouseInvert' in filtered && typeof filtered.mouseInvert !== 'boolean') {
+                            delete filtered.mouseInvert;
+                        }
                         const newConfig = { ...current, ...filtered };

@imxade
Copy link
Contributor

imxade commented Feb 10, 2026

I wanted someone else to do it

@alishair7071
Copy link
Contributor Author

alishair7071 commented Feb 10, 2026

@imxade is my PR not right? if someone other is ready to do it and you also wanna to make by some one else, no worry i am agreed

@imxade
Copy link
Contributor

imxade commented Feb 11, 2026

@imxade is my PR not right? if someone other is ready to do it and you also wanna to make by some one else, no worry i am agreed

I wanted you to guide someone else tackling these two issues

that someone else can be anyone without any merged pr but interested

@alishair7071
Copy link
Contributor Author

Ok @imxade, i think one of contributer commented on this issue to solve
I am here to help him, just assign issue to him, and i am happy to guide and help.

@imxade
Copy link
Contributor

imxade commented Feb 11, 2026

Ok @imxade, i think one of contributer commented on this issue to solve I am here to help him, just assign issue to him, and i am happy to guide and help.

no need for assignment just tell him he can go ahead with it, you have the ownership of these two issues

Edit: both of you are contributors, so I marked the issue as resolved

@imxade
Copy link
Contributor

imxade commented Feb 11, 2026

@alishair7071 only host, port and address are part of server config

make changes accordingly

@alishair7071
Copy link
Contributor Author

Ok @imxade, I wait for @aniket866 to solve this as you suggest, if he will not come in then i will change it by myself

@alishair7071
Copy link
Contributor Author

@imxade one other PR i have made PR #60 for second issue, if you want to merge you can review it. it is totally ready, and if you also want that PR to solved by some one else then its ok, i am happy to help

@imxade
Copy link
Contributor

imxade commented Feb 11, 2026

Ok @imxade, I wait for @aniket866 to solve this as you suggest, if he will not come in then i will change it by myself

do it yourself, I've closed that issue and reviewed the other pr

@alishair7071
Copy link
Contributor Author

Sir @imxade i have changed it accordingly by myself as you suggested that only host, port and address should be stored on server file, so it is ready you can review i, first we will solve this issue then we can move to second issue if no one will come even after 2 days waiting

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/routes/settings.tsx`:
- Around line 195-207: The label "Theme" is not programmatically associated with
the select, so update the Theme field to associate the label and control: add a
unique id (e.g., "theme-select") to the <select> and set the <label>'s htmlFor
to that id (or alternatively nest the <select> inside the <label>) so screen
readers can link the label to the control; ensure this change touches the select
element that uses value={theme} and onChange={(e) => setTheme(...)} and
references THEMES.DARK/THEMES.LIGHT.
- Around line 231-268: Add a saving guard and cleanup: introduce a component
state (e.g., isSaving) and check it at the start of the Save button's onClick
(referencing frontendPort and the button's onClick handler) to ignore additional
clicks and disable the button and show a loading indicator while saving; when
initiating the WebSocket (new WebSocket(wsUrl) / socket) set isSaving=true, and
on success/failure (socket.onopen, socket.onmessage error branch,
socket.onerror) set isSaving=false on failure and proceed with redirect on
success. Also store the socket and any setTimeout id in refs so you can close
socket and clearTimeout in a useEffect cleanup (to handle component unmount) and
ensure socket.close() is called on all terminal paths to avoid leaks and racing
redirects.
🧹 Nitpick comments (5)
src/hooks/useTrackpadGesture.ts (2)

19-27: Duplicate CLIENT_KEYS constants — extract to a shared module.

The same localStorage key strings (rein_mouse_sensitivity, rein_mouse_invert) are defined here and in src/routes/settings.tsx (lines 11–15). If either copy drifts, the settings page and gesture handler will silently disagree. Consider extracting these into a shared constants file (e.g., src/constants/clientKeys.ts).

Example shared module

Create src/constants/clientKeys.ts:

export const CLIENT_KEYS = {
    SENSITIVITY: 'rein_mouse_sensitivity',
    INVERT: 'rein_mouse_invert',
    THEME: 'rein-theme',
} as const;

export const CLIENT_DEFAULTS = {
    SENSITIVITY: 1.0,
    INVERT: false,
} as const;

Then import from both useTrackpadGesture.ts and settings.tsx.


29-33: Remove unused _defaultSensitivity parameter

The parameter accepts a default of 1.5 but is never read—getClientSettings() supplies its own default of 1.0. This creates a confusing API with conflicting default values.

Option A: Remove the parameter
 export const useTrackpadGesture = (
     send: (msg: any) => void,
     scrollMode: boolean,
-    _defaultSensitivity: number = 1.5
 ) => {
Option B: Use it as the fallback
-function getClientSettings(): { sensitivity: number; invert: boolean } {
-    if (typeof localStorage === 'undefined') return { sensitivity: 1.0, invert: false };
+function getClientSettings(defaultSensitivity: number = 1.0): { sensitivity: number; invert: boolean } {
+    if (typeof localStorage === 'undefined') return { sensitivity: defaultSensitivity, invert: false };
     const s = localStorage.getItem(CLIENT_KEYS.SENSITIVITY);
-    const sensitivity = s !== null && !Number.isNaN(Number(s)) ? Number(s) : 1.0;
+    const sensitivity = s !== null && !Number.isNaN(Number(s)) ? Number(s) : defaultSensitivity;
     const invert = localStorage.getItem(CLIENT_KEYS.INVERT) === 'true';
     return { sensitivity, invert };
 }
src/routes/settings.tsx (3)

26-29: The first-change guard pattern works but is verbose.

Three separate isFirst* refs to skip the initial effect run is a lot of boilerplate. A simpler alternative: check whether the new value actually differs from what's already in localStorage before writing, which removes the need for all isFirst* refs entirely.

Example: deduplicate with a write-if-changed helper
function persistIfChanged(key: string, value: string) {
    if (typeof window === 'undefined') return;
    if (localStorage.getItem(key) !== value) {
        localStorage.setItem(key, value);
    }
}

Then the persistence effects simplify to:

 useEffect(() => {
-    if (typeof window === 'undefined' || !hasLoadedFromStorage.current) return
-    if (isFirstSensitivity.current) { isFirstSensitivity.current = false; return }
-    localStorage.setItem(CLIENT_KEYS.SENSITIVITY, String(sensitivity))
+    if (!hasLoadedFromStorage.current) return
+    persistIfChanged(CLIENT_KEYS.SENSITIVITY, String(sensitivity))
 }, [sensitivity])

This eliminates isFirstSensitivity, isFirstInvert, and isFirstTheme refs.


76-88: QR code and display URL always reflect the current server port, not the user's input.

Both the QR URL (Line 81) and the display link (Line 289) use CONFIG.FRONTEND_PORT rather than the frontendPort state variable. This means if a user changes the port in the input but hasn't saved yet, the QR/link still show the old port. This is arguably correct (the new port isn't active until saved), but could confuse users. A small note or visual indicator that the QR reflects the current port might help.


118-120: Cleanup may miss a socket still in CONNECTING state.

The cleanup function only closes the socket when readyState === WebSocket.OPEN, but if the component unmounts while the socket is still connecting (CONNECTING state), it won't be closed. WebSocket.close() is valid to call in CONNECTING state.

Suggested fix
 return () => {
-    if (socket.readyState === WebSocket.OPEN) socket.close();
+    if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) socket.close();
 }

Comment on lines +195 to +207
<div className="form-control w-full">
<label className="label">
<span className="label-text">Theme</span>
</label>
<select
className="select select-bordered w-full"
value={theme}
onChange={(e) => setTheme(e.target.value as typeof THEMES.LIGHT | typeof THEMES.DARK)}
>
<option value={THEMES.DARK}>Dark (dracula)</option>
<option value={THEMES.LIGHT}>Light (cupcake)</option>
</select>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Accessibility: <label> is not associated with the <select> control.

Static analysis flagged this — the <label> on Line 196 has no htmlFor attribute and the <select> is not nested inside it. Screen readers won't associate the label text "Theme" with the dropdown.

Proposed fix
 <div className="form-control w-full">
-    <label className="label">
+    <label className="label" htmlFor="theme-select">
         <span className="label-text">Theme</span>
     </label>
     <select
+        id="theme-select"
         className="select select-bordered w-full"
         value={theme}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="form-control w-full">
<label className="label">
<span className="label-text">Theme</span>
</label>
<select
className="select select-bordered w-full"
value={theme}
onChange={(e) => setTheme(e.target.value as typeof THEMES.LIGHT | typeof THEMES.DARK)}
>
<option value={THEMES.DARK}>Dark (dracula)</option>
<option value={THEMES.LIGHT}>Light (cupcake)</option>
</select>
</div>
<div className="form-control w-full">
<label className="label" htmlFor="theme-select">
<span className="label-text">Theme</span>
</label>
<select
id="theme-select"
className="select select-bordered w-full"
value={theme}
onChange={(e) => setTheme(e.target.value as typeof THEMES.LIGHT | typeof THEMES.DARK)}
>
<option value={THEMES.DARK}>Dark (dracula)</option>
<option value={THEMES.LIGHT}>Light (cupcake)</option>
</select>
</div>
🧰 Tools
🪛 Biome (2.3.14)

[error] 196-198: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)

🤖 Prompt for AI Agents
In `@src/routes/settings.tsx` around lines 195 - 207, The label "Theme" is not
programmatically associated with the select, so update the Theme field to
associate the label and control: add a unique id (e.g., "theme-select") to the
<select> and set the <label>'s htmlFor to that id (or alternatively nest the
<select> inside the <label>) so screen readers can link the label to the
control; ensure this change touches the select element that uses value={theme}
and onChange={(e) => setTheme(...)} and references THEMES.DARK/THEMES.LIGHT.

Comment on lines 231 to +268
<button
className="btn btn-neutral w-full"
onClick={() => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
const wsUrl = `${protocol}//${host}/ws`;
const socket = new WebSocket(wsUrl);
const portNum = parseInt(frontendPort, 10)
if (Number.isNaN(portNum) || portNum < 1 || portNum > 65535) {
alert('Please enter a valid port (1–65535).')
return
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const host = window.location.host
const wsUrl = `${protocol}//${host}/ws`
const socket = new WebSocket(wsUrl)

socket.onerror = () => {
alert('Could not connect to server. Is the app running?')
}
socket.onopen = () => {
socket.send(JSON.stringify({
type: 'update-config',
config: {
frontendPort: parseInt(frontendPort),
mouseInvert: invertScroll,
mouseSensitivity: sensitivity,
config: { frontendPort: portNum },
}))
}
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
if (data.type === 'config-updated' && data.success) {
socket.close()
const newProtocol = window.location.protocol
const newHostname = window.location.hostname
const newUrl = `${newProtocol}//${newHostname}:${portNum}/settings`
setTimeout(() => { window.location.href = newUrl }, 800)
} else if (data.type === 'config-updated' && !data.success) {
alert('Failed to save config: ' + (data.error || 'Unknown error'))
}
}));

// Give server time to write config and restart
setTimeout(() => {
socket.close();
const newProtocol = window.location.protocol;
const newHostname = window.location.hostname;
const newUrl = `${newProtocol}//${newHostname}:${frontendPort}/settings`;
window.location.href = newUrl;
}, 1000);
};
} catch (_e) {
// ignore non-JSON messages
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Save button has no guard against double-clicks or loading feedback.

If the user clicks "Save Config" multiple times, each click opens a new WebSocket and sends a config update. This could cause multiple redirects racing or confusing error messages. Consider disabling the button while the save is in progress and showing a loading indicator.

Additionally, the WebSocket created in the onClick handler is not cleaned up if the component unmounts mid-save (e.g., user navigates away). This is a minor leak since the page will redirect anyway, but worth noting.

Sketch: add a saving guard
+const [saving, setSaving] = useState(false)
 ...
 <button
-    className="btn btn-neutral w-full"
+    className="btn btn-neutral w-full"
+    disabled={saving}
     onClick={() => {
+        if (saving) return
+        setSaving(true)
         const portNum = parseInt(frontendPort, 10)
         if (Number.isNaN(portNum) || portNum < 1 || portNum > 65535) {
             alert('Please enter a valid port (1–65535).')
+            setSaving(false)
             return
         }
         ...
+        socket.onerror = () => {
+            alert('Could not connect to server. Is the app running?')
+            setSaving(false)
+        }
     }}
 >
-    Save Config
+    {saving ? 'Saving…' : 'Save Config'}
 </button>
🧰 Tools
🪛 Biome (2.3.14)

[error] 231-271: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

🤖 Prompt for AI Agents
In `@src/routes/settings.tsx` around lines 231 - 268, Add a saving guard and
cleanup: introduce a component state (e.g., isSaving) and check it at the start
of the Save button's onClick (referencing frontendPort and the button's onClick
handler) to ignore additional clicks and disable the button and show a loading
indicator while saving; when initiating the WebSocket (new WebSocket(wsUrl) /
socket) set isSaving=true, and on success/failure (socket.onopen,
socket.onmessage error branch, socket.onerror) set isSaving=false on failure and
proceed with redirect on success. Also store the socket and any setTimeout id in
refs so you can close socket and clearTimeout in a useEffect cleanup (to handle
component unmount) and ensure socket.close() is called on all terminal paths to
avoid leaks and racing redirects.

@alishair7071
Copy link
Contributor Author

@imxade by the way i noticed that there are UI issues on light theme that is not the part of this issue/PR, we can solve that later

@imxade
Copy link
Contributor

imxade commented Feb 11, 2026

@imxade by the way i noticed that there are UI issues on light theme that is not the part of this issue/PR, we can solve that later

create a separate issue with image/video

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/hooks/useTrackpadGesture.ts`:
- Around line 19-27: getClientSettings currently treats an empty string in
localStorage as a valid sensitivity (Number('') === 0), which can
unintentionally disable movement; update the validation in getClientSettings
(and the CLIENT_KEYS.SENSITIVITY usage) to reject empty or non-positive values —
e.g., require s !== '' && Number(s) > 0 (or similar) before using Number(s),
otherwise fall back to the default 1.0 so sensitivity is never zero or negative.
- Around line 21-27: getClientSettings currently ignores the hook's
_defaultSensitivity parameter and always uses 1.0; either remove the unused
_defaultSensitivity from the hook signature or make getClientSettings accept a
defaultSensitivity argument and use it when localStorage has no valid
sensitivity. Concretely: modify getClientSettings to accept defaultSensitivity
(number) and replace the hardcoded 1.0 fallback with that parameter, then update
the call site that invokes getClientSettings to pass _defaultSensitivity (the
hook parameter); alternatively, remove _defaultSensitivity from the
useTrackpadGesture signature and all callers to avoid a misleading unused
parameter.
- Around line 158-172: In useTrackpadGesture's scrollMode branch the code
negates sumX/sumY into scrollDx/scrollDy and then multiplies by scrollSign,
causing double negation and inverted behavior; to fix, stop negating at
assignment (set scrollDx = sumX and scrollDy = sumY or otherwise remove the
leading '-'), keep the later multiplication by scrollSign/sensitivity when
building the 'scroll' message, and ensure variables scrollMode, sumX, sumY,
scrollDx, scrollDy and scrollSign are used as described so single- and
two-finger scrolls share the same direction convention.
🧹 Nitpick comments (1)
src/hooks/useTrackpadGesture.ts (1)

143-146: Reading localStorage on every touchmove event is unnecessary overhead.

getClientSettings() is called for every touch-move event, which fires at high frequency during gestures. While individual localStorage.getItem calls are fast, they are synchronous and this adds up. Consider reading the settings once at handleTouchStart (or caching in a ref that refreshes on touch-start) and reusing the values throughout the gesture.

♻️ Suggested approach (cache in refs, read on touch start)

Add refs alongside the existing ones:

+ const clientSensitivity = useRef(1.0);
+ const clientInvert = useRef(false);

Read at the start of each gesture in handleTouchStart:

  const handleTouchStart = (e: React.TouchEvent) => {
+     const cs = getClientSettings();
+     clientSensitivity.current = cs.sensitivity;
+     clientInvert.current = cs.invert;
      if (ongoingTouches.current.length === 0) {

Then in handleTouchMove, replace the per-event read:

-     const { sensitivity, invert } = getClientSettings();
-     const scrollSign = invert ? 1 : -1;
+     const sensitivity = clientSensitivity.current;
+     const scrollSign = clientInvert.current ? 1 : -1;

Comment on lines +19 to +27
const CLIENT_KEYS = { SENSITIVITY: 'rein_mouse_sensitivity', INVERT: 'rein_mouse_invert' } as const;

function getClientSettings(): { sensitivity: number; invert: boolean } {
if (typeof localStorage === 'undefined') return { sensitivity: 1.0, invert: false };
const s = localStorage.getItem(CLIENT_KEYS.SENSITIVITY);
const sensitivity = s !== null && !Number.isNaN(Number(s)) ? Number(s) : 1.0;
const invert = localStorage.getItem(CLIENT_KEYS.INVERT) === 'true';
return { sensitivity, invert };
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

getClientSettings helper is clean and SSR-safe — nice addition.

The typeof localStorage guard and the Number.isNaN validation are well done. One minor note: Number('') is 0 and Number.isNaN(0) is false, so an empty string in localStorage would parse as sensitivity 0 (effectively disabling movement). If that's undesirable, consider adding Number(s) > 0 or s !== '' to the guard.

🤖 Prompt for AI Agents
In `@src/hooks/useTrackpadGesture.ts` around lines 19 - 27, getClientSettings
currently treats an empty string in localStorage as a valid sensitivity
(Number('') === 0), which can unintentionally disable movement; update the
validation in getClientSettings (and the CLIENT_KEYS.SENSITIVITY usage) to
reject empty or non-positive values — e.g., require s !== '' && Number(s) > 0
(or similar) before using Number(s), otherwise fall back to the default 1.0 so
sensitivity is never zero or negative.

Comment on lines +21 to +27
function getClientSettings(): { sensitivity: number; invert: boolean } {
if (typeof localStorage === 'undefined') return { sensitivity: 1.0, invert: false };
const s = localStorage.getItem(CLIENT_KEYS.SENSITIVITY);
const sensitivity = s !== null && !Number.isNaN(Number(s)) ? Number(s) : 1.0;
const invert = localStorage.getItem(CLIENT_KEYS.INVERT) === 'true';
return { sensitivity, invert };
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

_defaultSensitivity parameter is accepted but never used; getClientSettings hardcodes its own default.

_defaultSensitivity (Line 32) is part of the public signature and callers may pass a custom value, but it is silently ignored — getClientSettings() always falls back to 1.0. Either plumb the default through:

-function getClientSettings(): { sensitivity: number; invert: boolean } {
-    if (typeof localStorage === 'undefined') return { sensitivity: 1.0, invert: false };
+function getClientSettings(defaultSensitivity: number = 1.0): { sensitivity: number; invert: boolean } {
+    if (typeof localStorage === 'undefined') return { sensitivity: defaultSensitivity, invert: false };
     const s = localStorage.getItem(CLIENT_KEYS.SENSITIVITY);
-    const sensitivity = s !== null && !Number.isNaN(Number(s)) ? Number(s) : 1.0;
+    const sensitivity = s !== null && !Number.isNaN(Number(s)) ? Number(s) : defaultSensitivity;

…and call it as getClientSettings(_defaultSensitivity) on Line 145, or remove the parameter from the hook signature to avoid misleading callers.

Also applies to: 32-33

🤖 Prompt for AI Agents
In `@src/hooks/useTrackpadGesture.ts` around lines 21 - 27, getClientSettings
currently ignores the hook's _defaultSensitivity parameter and always uses 1.0;
either remove the unused _defaultSensitivity from the hook signature or make
getClientSettings accept a defaultSensitivity argument and use it when
localStorage has no valid sensitivity. Concretely: modify getClientSettings to
accept defaultSensitivity (number) and replace the hardcoded 1.0 fallback with
that parameter, then update the call site that invokes getClientSettings to pass
_defaultSensitivity (the hook parameter); alternatively, remove
_defaultSensitivity from the useTrackpadGesture signature and all callers to
avoid a misleading unused parameter.

Comment on lines 158 to 172
} else if (scrollMode) {
// Scroll mode: single finger scrolls, or two-finger scroll in cursor mode
send({ type: 'scroll', dx: -sumX * sensitivity, dy: -sumY * sensitivity });
// Scroll mode: single finger scrolls; dominant axis (from main) for cleaner scroll
const scrollDx = -sumX;
const scrollDy = -sumY;
const absDx = Math.abs(scrollDx);
const absDy = Math.abs(scrollDy);
const useHorizontal = absDx > axisThreshold * absDy;
const useVertical = absDy > axisThreshold * absDx;
const dx = useHorizontal && !useVertical ? scrollDx : 0;
const dy = useVertical && !useHorizontal ? scrollDy : 0;
send({
type: 'scroll',
dx: Math.round(dx * sensitivity * scrollSign * 10) / 10,
dy: Math.round(dy * sensitivity * scrollSign * 10) / 10,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

invert setting works backwards in scrollMode due to the extra negation.

Lines 160–161 negate sumX/sumY (scrollDx = -sumX), but then Lines 170–171 also apply scrollSign (which already encodes inversion). The double negation means that when invert is false (scrollSign = −1), scrollMode scrolls in the opposite direction compared to 2-finger scroll — and toggling invert flips them out of sync.

Remove the upfront negation so both scroll paths share the same direction convention:

🐛 Proposed fix
             } else if (scrollMode) {
-                // Scroll mode: single finger scrolls; dominant axis (from main) for cleaner scroll
-                const scrollDx = -sumX;
-                const scrollDy = -sumY;
+                // Scroll mode: single finger scrolls; dominant axis for cleaner scroll
+                const scrollDx = sumX;
+                const scrollDy = sumY;
                 const absDx = Math.abs(scrollDx);
                 const absDy = Math.abs(scrollDy);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (scrollMode) {
// Scroll mode: single finger scrolls, or two-finger scroll in cursor mode
send({ type: 'scroll', dx: -sumX * sensitivity, dy: -sumY * sensitivity });
// Scroll mode: single finger scrolls; dominant axis (from main) for cleaner scroll
const scrollDx = -sumX;
const scrollDy = -sumY;
const absDx = Math.abs(scrollDx);
const absDy = Math.abs(scrollDy);
const useHorizontal = absDx > axisThreshold * absDy;
const useVertical = absDy > axisThreshold * absDx;
const dx = useHorizontal && !useVertical ? scrollDx : 0;
const dy = useVertical && !useHorizontal ? scrollDy : 0;
send({
type: 'scroll',
dx: Math.round(dx * sensitivity * scrollSign * 10) / 10,
dy: Math.round(dy * sensitivity * scrollSign * 10) / 10,
});
} else if (scrollMode) {
// Scroll mode: single finger scrolls; dominant axis for cleaner scroll
const scrollDx = sumX;
const scrollDy = sumY;
const absDx = Math.abs(scrollDx);
const absDy = Math.abs(scrollDy);
const useHorizontal = absDx > axisThreshold * absDy;
const useVertical = absDy > axisThreshold * absDx;
const dx = useHorizontal && !useVertical ? scrollDx : 0;
const dy = useVertical && !useHorizontal ? scrollDy : 0;
send({
type: 'scroll',
dx: Math.round(dx * sensitivity * scrollSign * 10) / 10,
dy: Math.round(dy * sensitivity * scrollSign * 10) / 10,
});
🤖 Prompt for AI Agents
In `@src/hooks/useTrackpadGesture.ts` around lines 158 - 172, In
useTrackpadGesture's scrollMode branch the code negates sumX/sumY into
scrollDx/scrollDy and then multiplies by scrollSign, causing double negation and
inverted behavior; to fix, stop negating at assignment (set scrollDx = sumX and
scrollDy = sumY or otherwise remove the leading '-'), keep the later
multiplication by scrollSign/sensitivity when building the 'scroll' message, and
ensure variables scrollMode, sumX, sumY, scrollDx, scrollDy and scrollSign are
used as described so single- and two-finger scrolls share the same direction
convention.

@alishair7071
Copy link
Contributor Author

@imxade resolved conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Save client-side settings on the client; use server-config.json only for server settings

2 participants