Skip to content

Enhance video call functionality and update dependencies - #3

Merged
mokoker merged 1 commit into
masterfrom
sprint9-26/Ui
May 13, 2026
Merged

Enhance video call functionality and update dependencies#3
mokoker merged 1 commit into
masterfrom
sprint9-26/Ui

Conversation

@tsimsekburgan

@tsimsekburgan tsimsekburgan commented May 12, 2026

Copy link
Copy Markdown
Contributor
  • Added support for video calls in the advisor and customer dashboards, including new routes and components for handling video calls.
  • Updated package.json and package-lock.json to include patch-package and other necessary dependencies.
  • Refactored RoleSelect.tsx to streamline advisor selection and manual login processes.
  • Improved API interaction in api.ts to allow for additional headers in instance retrieval.
  • Various UI enhancements and bug fixes across multiple components to improve user experience.

Summary by Sourcery

Add in-app video call pages for advisors and customers using LiveKit and integrate them with reservation workflows and navigation.

New Features:

  • Introduce dedicated advisor and customer video call pages that join LiveKit rooms based on reservation metadata and support background blur, camera/mic controls, and inline error handling.
  • Enable advisors to start video calls from appointments and reservation chat rooms, routing them into the new in-app video call experience instead of external windows.
  • Allow customers to join upcoming video calls from the dashboard via in-app navigation to the new video call page.

Enhancements:

  • Simplify advisor role selection by replacing server-fetched advisor lists with manual instance key entry, including validation and sensible fallbacks.
  • Extend the workflow instance API client to accept optional custom headers for authenticated video call polling and token retrieval.
  • Propagate LiveKit TURN server configuration and optional relay-only mode into client connect options for more robust media connectivity.
  • Increase the look-ahead window for highlighting upcoming customer reservations from 15 to 30 minutes.

Build:

  • Add patch-package as a development dependency and introduce a patch for livekit-client to support Morph gateway video tokens.

Chores:

  • Add utility modules for resolving LiveKit connection details from video call URLs, extracting video auth tokens from reservation instances, and unwrapping Morph-touch specific payload shapes.

Summary by CodeRabbit

  • New Features

    • Added dedicated video call pages for advisors and customers with full screen support, camera/microphone controls, and background blur options.
    • Implemented route-based video meeting navigation for seamless call initiation.
  • Refactor

    • Replaced modal-based video meeting flows with dedicated pages accessed via URLs.
    • Changed advisor role selection to manual key entry.
    • Extended upcoming reservation window from 15 to 30 minutes.

Review Change Stack

- Added support for video calls in the advisor and customer dashboards, including new routes and components for handling video calls.
- Updated `package.json` and `package-lock.json` to include `patch-package` and other necessary dependencies.
- Refactored `RoleSelect.tsx` to streamline advisor selection and manual login processes.
- Improved API interaction in `api.ts` to allow for additional headers in instance retrieval.
- Various UI enhancements and bug fixes across multiple components to improve user experience.
@tsimsekburgan
tsimsekburgan requested review from a team May 12, 2026 17:45
@sourcery-ai

sourcery-ai Bot commented May 12, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements in-app video call flows for advisors and customers via new LiveKit-based VideoCall pages, refactors role selection to use manual advisor keys instead of listing instances, wires new routes, and enhances API/utilities to support video call token handling and TURN configuration.

Sequence diagram for new in-app video call flow (advisor/customer)

sequenceDiagram
  actor User
  participant Dashboard as AdvisorDashboard/CustomerDashboard
  participant VideoCallPage as VideoCall
  participant API_getInstance as getInstance
  participant API_callFunction as callFunction
  participant LiveKitRoomComp as LiveKitRoom
  participant LiveKitServer

  User->>Dashboard: click Start meet (handleStartMeet)
  Dashboard->>API_callFunction: startInstance rezervation-start
  API_callFunction-->>Dashboard: result ok
  Dashboard->>VideoCallPage: navigate /{role}/video-call?rezervation=...

  VideoCallPage->>VideoCallPage: unwrapMorphTouchInstance
  loop poll rezervation
    VideoCallPage->>API_getInstance: getInstance rezervation rezervationId
    API_getInstance-->>VideoCallPage: instance data
    VideoCallPage->>VideoCallPage: extractTokenForVideoCall
    alt videoCallUrls present
      VideoCallPage->>VideoCallPage: resolveLiveKitFromVideoCallUrl
      VideoCallPage->>VideoCallPage: getLiveKitConnectOptions
      VideoCallPage->>LiveKitRoomComp: set serverUrl, token, connectOptions
      Note over VideoCallPage: break
    else livekit.room present
      VideoCallPage->>API_callFunction: callFunction check-livekit-room-access
      API_callFunction-->>VideoCallPage: token
      VideoCallPage->>VideoCallPage: getLiveKitServerUrl
      VideoCallPage->>LiveKitRoomComp: set serverUrl, token, connectOptions
      Note over VideoCallPage: break
    else no data yet
      VideoCallPage-->>VideoCallPage: wait and retry
    end
  end

  LiveKitRoomComp->>LiveKitServer: WebRTC connect (token)
  LiveKitServer-->>LiveKitRoomComp: media streams
  LiveKitRoomComp-->>User: render video call

  User-->>LiveKitRoomComp: leave call (AdvisorCallControls/CustomerVideoControls)
  LiveKitRoomComp-->>VideoCallPage: onDisconnected
  VideoCallPage->>Dashboard: navigate back
Loading

File-Level Changes

Change Details Files
Refactored RoleSelect advisor flow from dynamic instance listing to manual PM/IA key input with validation and simplified state.
  • Removed listInstances-based advisor fetching and related AdvisorItem/VnextInstance helpers and state.
  • Introduced separate PM/IA manual input fields with fallbacks, validation preventing keys starting with a digit, and inline error messaging.
  • Changed advisor selection handler to derive the advisor key from manual input or fallback and pass it to onAdvisorSelect.
  • Simplified role step navigation state and removed loading/error UI for advisors.
src/pages/RoleSelect.tsx
Reworked advisor and customer appointment video call initiation to navigate into new dedicated in-app video call pages instead of using polling modals and window.open.
  • Removed local videoCallModal state, polling useEffects, and modal UIs from advisor Appointments and customer Dashboard.
  • After successful rezervation-start, both flows now URL-encode the reservation id/key and navigate to role-specific /video-call routes.
  • Adjusted upcomingSoon window for customers from 15 to 30 minutes and updated empty-state copy accordingly.
src/pages/advisor/Appointments.tsx
src/pages/customer/Dashboard.tsx
Extended advisor chat management to detect rezervation chat rooms and deep-link into the new advisor video call page.
  • Added randevuKey/rezervationKey extraction in normalizeRoom and a helper to derive a rezervationId for video calls.
  • Rendered a conditional "Görüntülü görüşme" button for rezervation-type rooms that navigates to /advisor/video-call with the derived reservation id.
src/pages/advisor/ChatManagement.tsx
Wired new advisor and customer VideoCall pages into the router and barrel exports.
  • Imported AdvisorVideoCall and CustomerVideoCall components into App routing.
  • Registered /advisor/video-call and /customer/video-call routes.
  • Exported VideoCall from advisor and customer index modules.
src/App.tsx
src/pages/advisor/index.ts
src/pages/customer/index.ts
Enhanced getInstance API utility to support optional per-request headers for video-call–related polling and function calls.
  • Updated getInstance signature to accept an optional headers map and forward it to the underlying request call.
  • Kept existing callers compatible while allowing new video call flows to supply Authorization headers.
src/lib/api.ts
Extended advisor VideoCalls to pass LiveKit connect options derived from rezervation turnServers into the ActiveVideoCall LiveKitRoom.
  • Included turnServers in ReservationInstance attributes and plumbed RoomConnectOptions into ActiveVideoCall.
  • Applied getLiveKitConnectOptions result when constructing LiveKitRoom.
  • Ensured type compatibility with LiveKit's RoomConnectOptions.
src/pages/advisor/VideoCalls.tsx
Introduced new shared video call infrastructure utilities for LiveKit server configuration, Morph/matrix token handling, and URL resolution.
  • Added livekitConfig helpers to derive server URL (with env override) and build RoomConnectOptions from TURN server lists, including optional relay-only policy.
  • Implemented resolveLiveKitFromVideoCallUrl to extract LiveKit server URL and token (including Morph generate:room:user form) from videoCallUrls.
  • Created rezervationVideoToken helpers to extract bearer tokens and morph function string fields from workflow payloads.
  • Added unwrapMorphTouchInstance to normalize nested Morph instance shapes and livekitMorphToken to expose morph tokens via a global for patched livekit-client consumption.
src/lib/livekitConfig.ts
src/lib/resolveLiveKitFromVideoCallUrl.ts
src/lib/rezervationVideoToken.ts
src/lib/unwrapMorphTouchInstance.ts
src/lib/livekitMorphToken.ts
Implemented dedicated advisor and customer VideoCall pages that poll rezervation instances, resolve LiveKit connection data, and host the in-app LiveKitRoom UI with background blur and error handling.
  • Both VideoCall components read rezervation id from query params and enforce presence of advisor/customer session context, surfacing friendly error states and navigation back to dashboards.
  • They poll rezervation workflow instances with optional bearer auth, unwrap Morph payloads, extract videoCallUrls, turnServers, and webrtcIntegration, and attempt to resolve LiveKit server/token via URL or function-based room access (check-livekit-room-access).
  • On successful resolution they configure LiveKitRoom (serverUrl, token, connectOptions), set global morph token where needed, and render symmetric UIs: remote video/screen as main, local camera PIP, camera/mic toggles, optional background blur via livekit track processors, and leave buttons that route back.
  • They implement time-bounded polling with retry and timeout messaging for missing video links.
src/pages/advisor/VideoCall.tsx
src/pages/customer/VideoCall.tsx
Updated dependencies to support patching livekit-client for Morph integration.
  • Added patch-package as a devDependency.
  • Introduced a patch file for livekit-client 2.17.3 (contents not shown in diff) to integrate Morph-specific behaviour such as reading global morph tokens.
package.json
package-lock.json
patches/livekit-client+2.17.3.patch

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements a complete LiveKit-based video calling feature for advisor and customer flows, refactoring from in-modal polling to route-based full-page video call pages with managed connection lifecycle and token authentication.

Changes

LiveKit Video Calling

Layer / File(s) Summary
LiveKit library patch and configuration
package.json, patches/livekit-client+2.17.3.patch, src/lib/livekitConfig.ts, src/lib/livekitMorphToken.ts
Patch-package dependency added; livekit-client patched for room_token redaction and morph-token injection into connection params; LiveKit server URL and connection option helpers exported with environment fallbacks and TURN server validation.
Video token and data extraction utilities
src/lib/resolveLiveKitFromVideoCallUrl.ts, src/lib/rezervationVideoToken.ts, src/lib/unwrapMorphTouchInstance.ts
URL-to-LiveKit-join resolver supporting Morph/Burgan (room+user params) and legacy JWT formats; Bearer auth header builder; token extraction from nested payload fields; recursive instance payload unwrapper for consistent normalization.
API enhancement for authenticated polling
src/lib/api.ts
getInstance signature updated to accept optional headers parameter, enabling Bearer-authenticated requests during video call setup phases.
Routing and page exports
src/App.tsx, src/pages/advisor/index.ts, src/pages/customer/index.ts
New /advisor/video-call and /customer/video-call routes added to authenticated layout; VideoCall components exported from advisor and customer page modules.
Advisor video call and navigation
src/pages/advisor/VideoCall.tsx, src/pages/advisor/Appointments.tsx, src/pages/advisor/ChatManagement.tsx
Advisor VideoCall page with full LiveKit integration (polling with header auth, remote stage preferring screenshare, local preview thumbnail, camera/mic/blur controls, error handling). Appointments component navigates to video call on meeting start. ChatManagement adds conditional video-call button for reservation rooms and computes reservation ID from randevuKey/rezervationKey.
Customer video call and navigation
src/pages/customer/VideoCall.tsx, src/pages/customer/Dashboard.tsx
Customer VideoCall page with full LiveKit integration and same controls/error handling. Dashboard component navigates to video call on meeting start, removes in-modal polling, and expands upcoming meeting window from 15 to 30 minutes.
Role selection UI refactoring
src/pages/RoleSelect.tsx
Advisor selection changed from fetching advisor list to manual instance-key login with fixed fallback keys for PM and IA; validation requires keys to start with digit.
Existing video calls integration
src/pages/advisor/VideoCalls.tsx
ActiveVideoCall updated to compute and pass LiveKit RoomConnectOptions from reservation turnServers to <LiveKitRoom />.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • burgan-tech/morph-touch-ui#1: Both PRs modify src/lib/api.ts at the API call/signature level; this PR adds optional headers support to getInstance for authenticated video call polling.

Poem

🐰 Whiskers twitching with glee,
Video calls now flow so free,
Routes replace the modal dance,
LiveKit tokens in the glance,
Hop-hop-hooray for calls today! 🎥✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.84% 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 'Enhance video call functionality and update dependencies' accurately summarizes the main changes: adding video call components/routes and updating package.json with patch-package.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sprint9-26/Ui

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.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 security issues, 2 other issues, and left some high level feedback:

Security issues:

  • Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections. (link)
  • Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections. (link)

General comments:

  • The new advisor and customer VideoCall components share a lot of almost identical logic (polling rezervation, resolving LiveKit URL/token, rendering controls/stage); consider extracting shared hooks/utilities (e.g. a generic useRezervationVideoCall + shared call layout components) to reduce duplication and keep behavior changes in one place.
  • The polling logic in both VideoCall components uses multiple mutable refs (cancelledRef, timerRef, pollAttemptsRef, videoAuthRef) and inline schedule/poll definitions; extracting this into a reusable helper or hook with clearer state (e.g. using useRef + a single cleanup function) would make the flow easier to reason about and less error-prone.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new advisor and customer `VideoCall` components share a lot of almost identical logic (polling rezervation, resolving LiveKit URL/token, rendering controls/stage); consider extracting shared hooks/utilities (e.g. a generic `useRezervationVideoCall` + shared call layout components) to reduce duplication and keep behavior changes in one place.
- The polling logic in both `VideoCall` components uses multiple mutable refs (`cancelledRef`, `timerRef`, `pollAttemptsRef`, `videoAuthRef`) and inline `schedule`/`poll` definitions; extracting this into a reusable helper or hook with clearer state (e.g. using `useRef` + a single cleanup function) would make the flow easier to reason about and less error-prone.

## Individual Comments

### Comment 1
<location path="src/pages/advisor/VideoCall.tsx" line_range="227-33" />
<code_context>
+export function VideoCall() {
</code_context>
<issue_to_address>
**suggestion:** Advisor VideoCall contains a lot of logic duplicated in Customer VideoCall; consider extracting shared hooks/components

These two components share almost all the same polling, token resolution, error/phase handling, LiveKit wiring, and blur/local preview logic. Keeping them separate increases the risk they drift in behavior (e.g., retry limits, headers, TURN config) and raises maintenance cost. Extracting a shared core (e.g., a `useLiveKitRezervationCall` hook plus generic `ActiveCall`/control components with role-specific labels) would make this easier to evolve safely.

Suggested implementation:

```typescript
    </LiveKitRoom>
  );
}

type RezervationPhase = 'poll' | 'live' | 'error';
type RezervationRole = 'advisor' | 'customer';

interface UseRezervationLiveKitCallOptions {
  rezervationId: string;
  role: RezervationRole;
  navigate: ReturnType<typeof useNavigate>;
}

/**
 * Shared core hook for managing a rezervation-based LiveKit call.
 *
 * This centralizes the state shape (phase, errors, tokens, LiveKit server URL, etc)
 * so it can be reused by both advisor and customer VideoCall flows.
 *
 * The polling, token resolution, and LiveKit wiring that currently live in the
 * concrete VideoCall components can be migrated into this hook to fully deduplicate
 * behavior.
 */
function useRezervationLiveKitCall(
  _options: UseRezervationLiveKitCallOptions,
) {
  const [phase, setPhase] = useState<RezervationPhase>('poll');
  const [errorMsg, setErrorMsg] = useState<string | null>(null);
  const [token, setToken] = useState<string | null>(null);
  const [serverUrl, setServerUrl] = useState<string | null>(null);
  const [morphVideoToken, setMorphVideoToken] = useState<string | null>(null);

  return {
    phase,
    setPhase,
    errorMsg,
    setErrorMsg,
    token,
    setToken,
    serverUrl,
    setServerUrl,
    morphVideoToken,
    setMorphVideoToken,
  };
}

export function VideoCall() {

```

```typescript
  const ADVISOR_ID = useAdvisorContext().advisorId!;
  const rezervationId = searchParams.get('rezervation')?.trim() ?? '';

  const {
    phase,
    setPhase,
    errorMsg,
    setErrorMsg,
    token,
    setToken,
    serverUrl,
    setServerUrl,
    morphVideoToken,
    setMorphVideoToken,
  } = useRezervationLiveKitCall({
    rezervationId,
    role: 'advisor',
    navigate,
  });

```

To fully implement the deduplication you suggested, you should also:
1. Import and use `useRezervationLiveKitCall` in the customer `VideoCall` (e.g., `src/pages/customer/VideoCall.tsx`) and refactor that component to consume the same returned state (phase, errorMsg, token, serverUrl, morphVideoToken, and their setters).
2. Move the shared polling/token-resolution/LiveKit wiring and error/phase handling logic from both advisor and customer `VideoCall` components into `useRezervationLiveKitCall`:
   - Accept any role-specific parameters via `UseRezervationLiveKitCallOptions` (e.g., headers, TURN config, retry limits).
   - Implement the shared `useEffect` for polling the rezervation status, resolving the LiveKit token/server URL, and wiring the morph/blur/local-preview logic.
   - Keep only role-specific labeling or minor UI affordances in the individual `VideoCall` components.
3. Optionally extract the shared “active call” layout (e.g., the `<LiveKitRoom>` wrapper and the stage/controls layout) into a reusable `RezervationActiveCall` component used by both advisor and customer flows, parameterized by role-specific subcomponents (such as `AdvisorCallControls` vs `CustomerCallControls`).
</issue_to_address>

### Comment 2
<location path="src/lib/livekitConfig.ts" line_range="23-35" />
<code_context>
+  return typeof urls === 'string' || (Array.isArray(urls) && urls.every((url) => typeof url === 'string'));
+}
+
+export function getLiveKitConnectOptions(turnServers: unknown): RoomConnectOptions | undefined {
+  if (!Array.isArray(turnServers)) return undefined;
+
+  const iceServers = turnServers.filter(isValidTurnServer);
+  if (iceServers.length === 0) return undefined;
+
+  return {
+    rtcConfig: {
+      iceServers,
+      ...(shouldForceRelay() ? { iceTransportPolicy: 'relay' as const } : {}),
+    },
+  };
+}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Currently only array-shaped turnServers are considered; silently ignoring non-array values may hide misconfigurations

If `turnServers` is non-null but not an array, we immediately return `undefined` and the client silently falls back to LiveKit defaults. That means a misconfigured backend (e.g., sending a single object instead of an array) will degrade connectivity without any visible signal. Consider logging or at least `console.warn` in this case so misconfigurations are easier to detect.

```suggestion
export function getLiveKitConnectOptions(turnServers: unknown): RoomConnectOptions | undefined {
  if (turnServers == null) return undefined;

  if (!Array.isArray(turnServers)) {
    // Non-null, non-array turnServers likely indicates a backend misconfiguration.
    // Log a warning so this is visible during development/monitoring.
    // eslint-disable-next-line no-console
    console.warn(
      '[livekitConfig] Expected `turnServers` to be an array or null/undefined, ' +
        `but received value of type "${typeof turnServers}". Falling back to LiveKit defaults.`,
      turnServers,
    );
    return undefined;
  }

  const iceServers = turnServers.filter(isValidTurnServer);
  if (iceServers.length === 0) return undefined;

  return {
    rtcConfig: {
      iceServers,
      ...(shouldForceRelay() ? { iceTransportPolicy: 'relay' as const } : {}),
    },
  };
}
```
</issue_to_address>

### Comment 3
<location path="src/lib/resolveLiveKitFromVideoCallUrl.ts" line_range="14" />
<code_context>
        ? `ws://${url.host}`
</code_context>
<issue_to_address>
**security (javascript.lang.security.detect-insecure-websocket):** Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.

*Source: opengrep*
</issue_to_address>

### Comment 4
<location path="src/lib/resolveLiveKitFromVideoCallUrl.ts" line_range="18" />
<code_context>
            ? `ws://${url.host}`
</code_context>
<issue_to_address>
**security (javascript.lang.security.detect-insecure-websocket):** Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

videoCallUrls?: Record<string, string>[];
turnServers?: unknown[];
webrtcIntegration?: { livekit?: { room?: string } };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Advisor VideoCall contains a lot of logic duplicated in Customer VideoCall; consider extracting shared hooks/components

These two components share almost all the same polling, token resolution, error/phase handling, LiveKit wiring, and blur/local preview logic. Keeping them separate increases the risk they drift in behavior (e.g., retry limits, headers, TURN config) and raises maintenance cost. Extracting a shared core (e.g., a useLiveKitRezervationCall hook plus generic ActiveCall/control components with role-specific labels) would make this easier to evolve safely.

Suggested implementation:

    </LiveKitRoom>
  );
}

type RezervationPhase = 'poll' | 'live' | 'error';
type RezervationRole = 'advisor' | 'customer';

interface UseRezervationLiveKitCallOptions {
  rezervationId: string;
  role: RezervationRole;
  navigate: ReturnType<typeof useNavigate>;
}

/**
 * Shared core hook for managing a rezervation-based LiveKit call.
 *
 * This centralizes the state shape (phase, errors, tokens, LiveKit server URL, etc)
 * so it can be reused by both advisor and customer VideoCall flows.
 *
 * The polling, token resolution, and LiveKit wiring that currently live in the
 * concrete VideoCall components can be migrated into this hook to fully deduplicate
 * behavior.
 */
function useRezervationLiveKitCall(
  _options: UseRezervationLiveKitCallOptions,
) {
  const [phase, setPhase] = useState<RezervationPhase>('poll');
  const [errorMsg, setErrorMsg] = useState<string | null>(null);
  const [token, setToken] = useState<string | null>(null);
  const [serverUrl, setServerUrl] = useState<string | null>(null);
  const [morphVideoToken, setMorphVideoToken] = useState<string | null>(null);

  return {
    phase,
    setPhase,
    errorMsg,
    setErrorMsg,
    token,
    setToken,
    serverUrl,
    setServerUrl,
    morphVideoToken,
    setMorphVideoToken,
  };
}

export function VideoCall() {
  const ADVISOR_ID = useAdvisorContext().advisorId!;
  const rezervationId = searchParams.get('rezervation')?.trim() ?? '';

  const {
    phase,
    setPhase,
    errorMsg,
    setErrorMsg,
    token,
    setToken,
    serverUrl,
    setServerUrl,
    morphVideoToken,
    setMorphVideoToken,
  } = useRezervationLiveKitCall({
    rezervationId,
    role: 'advisor',
    navigate,
  });

To fully implement the deduplication you suggested, you should also:

  1. Import and use useRezervationLiveKitCall in the customer VideoCall (e.g., src/pages/customer/VideoCall.tsx) and refactor that component to consume the same returned state (phase, errorMsg, token, serverUrl, morphVideoToken, and their setters).
  2. Move the shared polling/token-resolution/LiveKit wiring and error/phase handling logic from both advisor and customer VideoCall components into useRezervationLiveKitCall:
    • Accept any role-specific parameters via UseRezervationLiveKitCallOptions (e.g., headers, TURN config, retry limits).
    • Implement the shared useEffect for polling the rezervation status, resolving the LiveKit token/server URL, and wiring the morph/blur/local-preview logic.
    • Keep only role-specific labeling or minor UI affordances in the individual VideoCall components.
  3. Optionally extract the shared “active call” layout (e.g., the <LiveKitRoom> wrapper and the stage/controls layout) into a reusable RezervationActiveCall component used by both advisor and customer flows, parameterized by role-specific subcomponents (such as AdvisorCallControls vs CustomerCallControls).

Comment thread src/lib/livekitConfig.ts
Comment on lines +23 to +35
export function getLiveKitConnectOptions(turnServers: unknown): RoomConnectOptions | undefined {
if (!Array.isArray(turnServers)) return undefined;

const iceServers = turnServers.filter(isValidTurnServer);
if (iceServers.length === 0) return undefined;

return {
rtcConfig: {
iceServers,
...(shouldForceRelay() ? { iceTransportPolicy: 'relay' as const } : {}),
},
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Currently only array-shaped turnServers are considered; silently ignoring non-array values may hide misconfigurations

If turnServers is non-null but not an array, we immediately return undefined and the client silently falls back to LiveKit defaults. That means a misconfigured backend (e.g., sending a single object instead of an array) will degrade connectivity without any visible signal. Consider logging or at least console.warn in this case so misconfigurations are easier to detect.

Suggested change
export function getLiveKitConnectOptions(turnServers: unknown): RoomConnectOptions | undefined {
if (!Array.isArray(turnServers)) return undefined;
const iceServers = turnServers.filter(isValidTurnServer);
if (iceServers.length === 0) return undefined;
return {
rtcConfig: {
iceServers,
...(shouldForceRelay() ? { iceTransportPolicy: 'relay' as const } : {}),
},
};
}
export function getLiveKitConnectOptions(turnServers: unknown): RoomConnectOptions | undefined {
if (turnServers == null) return undefined;
if (!Array.isArray(turnServers)) {
// Non-null, non-array turnServers likely indicates a backend misconfiguration.
// Log a warning so this is visible during development/monitoring.
// eslint-disable-next-line no-console
console.warn(
'[livekitConfig] Expected `turnServers` to be an array or null/undefined, ' +
`but received value of type "${typeof turnServers}". Falling back to LiveKit defaults.`,
turnServers,
);
return undefined;
}
const iceServers = turnServers.filter(isValidTurnServer);
if (iceServers.length === 0) return undefined;
return {
rtcConfig: {
iceServers,
...(shouldForceRelay() ? { iceTransportPolicy: 'relay' as const } : {}),
},
};
}

p === 'https:'
? `wss://${url.host}`
: p === 'http:'
? `ws://${url.host}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security (javascript.lang.security.detect-insecure-websocket): Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.

Source: opengrep

: p === 'wss:'
? `wss://${url.host}`
: p === 'ws:'
? `ws://${url.host}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security (javascript.lang.security.detect-insecure-websocket): Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.

Source: opengrep

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request integrates a native video calling experience using LiveKit, replacing the previous external URL redirection logic. It introduces patch-package to modify livekit-client for specialized token handling and adds several utility libraries for resolving LiveKit parameters and extracting authentication tokens from workflow payloads. New dedicated video call routes and pages were added for both advisors and customers, and the RoleSelect page was refactored to use manual instance keys. Feedback focuses on the significant code duplication between the advisor and customer video call components and the security implications of storing authentication tokens in the global execution context.

Comment on lines +1 to +423
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { Video, PhoneOff, Scan } from 'lucide-react';
import { Track, type RoomConnectOptions } from 'livekit-client';
import {
LiveKitRoom,
VideoTrack,
TrackToggle,
RoomAudioRenderer,
useTracks,
useLocalParticipant,
} from '@livekit/components-react';
import '@livekit/components-styles';
import { BackgroundBlur, supportsBackgroundProcessors } from '@livekit/track-processors';
import { callFunction, getInstance } from '../../lib/api';
import { getLiveKitConnectOptions, getLiveKitServerUrl } from '../../lib/livekitConfig';
import { resolveLiveKitFromVideoCallUrl } from '../../lib/resolveLiveKitFromVideoCallUrl';
import {
bearerAuthForVideoCall,
extractMorphFunctionStringField,
extractTokenForVideoCall,
} from '../../lib/rezervationVideoToken';
import { unwrapMorphTouchInstance } from '../../lib/unwrapMorphTouchInstance';
import { setLiveKitMorphVideoToken } from '../../lib/livekitMorphToken';
import { cn } from '../../lib/utils';
import { toast } from '../../components/ui';
import { useAdvisorContext } from '../../contexts/AdvisorContext';

interface RezervationAttributes {
videoCallUrls?: Record<string, string>[];
turnServers?: unknown[];
webrtcIntegration?: { livekit?: { room?: string } };
}

function extractAttributes(data: unknown): RezervationAttributes {
if (!data || typeof data !== 'object') return {};
const d = data as Record<string, unknown>;
const attrs = (d.attributes as Record<string, unknown> | undefined) ?? {};
return {
videoCallUrls: (attrs.videoCallUrls ?? d.videoCallUrls) as Record<string, string>[] | undefined,
turnServers: (attrs.turnServers ?? d.turnServers) as unknown[] | undefined,
webrtcIntegration: (attrs.webrtcIntegration ?? d.webrtcIntegration) as RezervationAttributes['webrtcIntegration'],
};
}

function AdvisorCallControls({ onLeave }: { onLeave: () => void }) {
const [blurEnabled, setBlurEnabled] = useState(false);
const { localParticipant } = useLocalParticipant();

const toggleBlur = useCallback(async () => {
if (!supportsBackgroundProcessors()) {
toast('Arka plan bulanıklaştırma bu tarayıcıda desteklenmiyor', 'error');
return;
}
try {
const camPub = localParticipant.getTrackPublication(Track.Source.Camera);
const track = camPub?.track;
if (!track) return;
const videoTrack = track as import('livekit-client').LocalVideoTrack;
if (blurEnabled) {
await videoTrack.stopProcessor();
} else {
await videoTrack.setProcessor(BackgroundBlur(10));
}
setBlurEnabled(!blurEnabled);
} catch (e) {
toast(String(e), 'error');
}
}, [blurEnabled, localParticipant]);

return (
<div className="video-controls">
<TrackToggle
source={Track.Source.Camera}
className={cn('video-controls', 'btn')}
title="Kamera"
/>
<TrackToggle
source={Track.Source.Microphone}
className={cn('video-controls', 'btn')}
title="Mikrofon"
/>
<button
type="button"
className={blurEnabled ? 'active' : 'inactive'}
onClick={toggleBlur}
title="Arka plan bulanıklaştırma"
>
<Scan size={18} />
</button>
<button type="button" className="end-call" onClick={onLeave} title="Görüşmeyi bitir">
<PhoneOff size={20} />
</button>
</div>
);
}

function RemoteCustomerStage() {
const tracks = useTracks([Track.Source.Camera, Track.Source.ScreenShare]);
const remote = tracks.filter((ref) => ref.participant && !ref.participant.isLocal);
const screenShare = remote.find((t) => t.source === Track.Source.ScreenShare);
const mainTrack = screenShare ?? remote[0];

return (
<div
className="video-call-main"
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
position: 'relative',
background: '#0f172a',
minHeight: 0,
}}
>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{mainTrack ? (
<VideoTrack trackRef={mainTrack} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
) : (
<div className="empty-state" style={{ color: '#94a3b8' }}>
<Video size={48} strokeWidth={1.5} />
<p>Müşteri video akışı bekleniyor</p>
</div>
)}
</div>
</div>
);
}

function LocalCameraPreview() {
const tracks = useTracks([Track.Source.Camera]);
const local = tracks.find((ref) => ref.participant?.isLocal && ref.source === Track.Source.Camera);
if (!local) return null;
return (
<div
style={{
position: 'absolute',
right: 16,
bottom: 88,
width: 140,
height: 105,
borderRadius: 8,
overflow: 'hidden',
border: '2px solid rgba(255,255,255,0.25)',
boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
zIndex: 2,
background: '#000',
}}
>
<VideoTrack trackRef={local} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
</div>
);
}

function ActiveAdvisorCall({
serverUrl,
token,
morphVideoToken,
connectOptions,
onLeave,
onCallFailure,
}: {
serverUrl: string;
token: string;
morphVideoToken: string | null;
connectOptions?: RoomConnectOptions;
onLeave: () => void;
onCallFailure: (message: string) => void;
}) {
const userInitiatedLeave = useRef(false);
const failureReported = useRef(false);

const reportFailureOnce = useCallback(
(message: string) => {
if (failureReported.current) return;
failureReported.current = true;
onCallFailure(message);
},
[onCallFailure],
);

const handleUserLeave = useCallback(() => {
userInitiatedLeave.current = true;
onLeave();
}, [onLeave]);

const handleRoomDisconnected = useCallback(() => {
if (userInitiatedLeave.current) return;
reportFailureOnce('Görüşme bağlantısı kesildi.');
}, [reportFailureOnce]);

useLayoutEffect(() => {
setLiveKitMorphVideoToken(morphVideoToken);
return () => setLiveKitMorphVideoToken(null);
}, [morphVideoToken]);

return (
<LiveKitRoom
serverUrl={serverUrl}
token={token}
connect
video
audio
connectOptions={connectOptions}
onDisconnected={handleRoomDisconnected}
onError={(err) => {
console.error('[LiveKit]', err);
toast(err.message, 'error');
reportFailureOnce(err.message || String(err));
}}
style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
>
<RoomAudioRenderer />
<div className="video-call-container" style={{ flex: 1, minHeight: 0, position: 'relative' }}>
<div className="video-call-main" style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<div style={{ flex: 1, position: 'relative', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<RemoteCustomerStage />
<LocalCameraPreview />
<AdvisorCallControls onLeave={handleUserLeave} />
</div>
</div>
</div>
</LiveKitRoom>
);
}

export function VideoCall() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const ADVISOR_ID = useAdvisorContext().advisorId!;
const rezervationId = searchParams.get('rezervation')?.trim() ?? '';

const [phase, setPhase] = useState<'poll' | 'live' | 'error'>('poll');
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [token, setToken] = useState<string | null>(null);
const [serverUrl, setServerUrl] = useState<string | null>(null);
const [morphVideoToken, setMorphVideoToken] = useState<string | null>(null);
const [connectOptions, setConnectOptions] = useState<RoomConnectOptions | undefined>();

const goDashboard = useCallback(() => {
navigate('/advisor', { replace: true });
}, [navigate]);

const handleCallFailure = useCallback((message: string) => {
console.error('[VideoCall]', message);
setErrorMsg(message);
setPhase('error');
setToken(null);
setServerUrl(null);
setMorphVideoToken(null);
setConnectOptions(undefined);
}, []);

useEffect(() => {
if (!rezervationId) {
setErrorMsg('Randevu bilgisi eksik.');
setPhase('error');
return;
}
if (!ADVISOR_ID) {
setErrorMsg('Oturum bulunamadı; lütfen tekrar giriş yapın.');
setPhase('error');
return;
}

const cancelledRef = { current: false };
const timerRef = { current: undefined as ReturnType<typeof setTimeout> | undefined };
const videoAuthRef = { current: null as string | null };
const pollAttemptsRef = { current: 0 };

const schedule = (fn: () => void, ms: number) => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(fn, ms);
};

const tryFetchTokenViaRoom = async (roomName: string): Promise<string | null> => {
try {
const res = await callFunction(
'check-livekit-room-access',
{
roomName,
advisorId: ADVISOR_ID,
},
bearerAuthForVideoCall(videoAuthRef.current),
);
if (!res.ok) return null;
return extractMorphFunctionStringField(res.data, 'token');
} catch {
return null;
}
};

const poll = async () => {
if (cancelledRef.current) return;
pollAttemptsRef.current += 1;
if (pollAttemptsRef.current > 50) {
setErrorMsg(
'Görüşme bağlantısı zaman aşımına uğradı. Randevu kaydında video bağlantısı yoksa danışmanın görüşmeyi başlatması gerekir.',
);
setPhase('error');
return;
}
try {
const res = await getInstance(
'rezervation',
rezervationId,
bearerAuthForVideoCall(videoAuthRef.current),
);
if (cancelledRef.current) return;
if (!res.ok) {
schedule(() => void poll(), 3000);
return;
}
const instanceRoot = unwrapMorphTouchInstance(res.data);
const morphTok = extractTokenForVideoCall(instanceRoot);
if (morphTok) videoAuthRef.current = morphTok;
const { videoCallUrls, turnServers, webrtcIntegration } = extractAttributes(instanceRoot);
const liveKitConnectOptions = getLiveKitConnectOptions(turnServers);
if (videoCallUrls && Array.isArray(videoCallUrls) && videoCallUrls.length > 0) {
const myEntry =
videoCallUrls.find((u) => u && ADVISOR_ID in u) ??
videoCallUrls.find((u) => u && Object.keys(u).length > 0);
const rawCandidates: string[] = [];
if (myEntry) {
const mine = myEntry[ADVISOR_ID];
if (typeof mine === 'string' && mine.length > 0) rawCandidates.push(mine);
for (const v of Object.values(myEntry)) {
if (typeof v === 'string' && v.length > 0 && !rawCandidates.includes(v)) rawCandidates.push(v);
}
}
for (const raw of rawCandidates) {
const resolved = resolveLiveKitFromVideoCallUrl(raw);
if (resolved) {
const usesMorphGateway = resolved.token.startsWith('generate:');
if (usesMorphGateway && !morphTok) {
break;
}
setMorphVideoToken(usesMorphGateway ? morphTok : null);
setConnectOptions(liveKitConnectOptions);
setServerUrl(resolved.serverUrl);
setToken(resolved.token);
setPhase('live');
return;
}
}
}
const roomName = webrtcIntegration?.livekit?.room;
if (roomName && typeof roomName === 'string' && roomName.length > 0) {
const t = await tryFetchTokenViaRoom(roomName);
if (!cancelledRef.current && t) {
setMorphVideoToken(null);
setConnectOptions(liveKitConnectOptions);
setServerUrl(getLiveKitServerUrl());
setToken(t);
setPhase('live');
return;
}
}
} catch {
/* retry */
}
if (!cancelledRef.current) schedule(() => void poll(), 3000);
};

schedule(() => void poll(), 1500);

return () => {
cancelledRef.current = true;
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [rezervationId, ADVISOR_ID]);

if (!rezervationId || phase === 'error') {
return (
<div className="page" style={{ maxWidth: 480, margin: '0 auto', padding: 24 }}>
<div className="empty-state">
<Video size={40} strokeWidth={1.5} />
<p style={{ fontWeight: 600 }}>Görüntülü görüşme açılamadı</p>
<p className="text-muted text-sm">{errorMsg ?? 'Geçersiz bağlantı.'}</p>
<button type="button" className="btn btn-primary mt-3" onClick={goDashboard}>
Panele dön
</button>
</div>
</div>
);
}

if (phase === 'poll' || !token || !serverUrl) {
return (
<div className="page" style={{ height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
<Video size={48} strokeWidth={1.5} style={{ marginBottom: 16, color: 'var(--color-primary)' }} />
<p style={{ fontWeight: 600, marginBottom: 8 }}>Görüşme hazırlanıyor</p>
<p className="text-muted text-sm text-center">Bağlantı bilgileri alınıyor; lütfen bekleyin.</p>
<div
className="animate-spin mt-6"
style={{
width: 28,
height: 28,
border: '3px solid var(--color-border)',
borderTopColor: 'var(--color-primary)',
borderRadius: '50%',
}}
/>
<button type="button" className="btn btn-secondary mt-8" onClick={goDashboard}>
İptal
</button>
</div>
);
}

return (
<div className="page" style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<ActiveAdvisorCall
serverUrl={serverUrl}
token={token}
morphVideoToken={morphVideoToken}
connectOptions={connectOptions}
onLeave={goDashboard}
onCallFailure={handleCallFailure}
/>
</div>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The VideoCall.tsx component in the advisor dashboard is almost identical to the one in the customer dashboard (src/pages/customer/VideoCall.tsx). This significant code duplication (over 400 lines) makes maintenance difficult and increases the risk of bugs if logic changes are only applied to one side.

Please refactor the common logic (polling, LiveKit resolution, track handling, and UI structure) into a shared component or a custom hook. You can pass labels, context-specific IDs, and navigation paths as props or parameters.

Comment on lines +20 to +26
+ var morph = typeof globalThis !== 'undefined' && globalThis.__morphTouchLiveKitMorphToken;
+ if (morph && typeof morph === 'string' && morph.length > 0) {
+ params.set('access_token', morph);
+ params.set('room_token', token);
+ } else {
+ params.set('access_token', token);
+ }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

Storing sensitive authentication tokens in globalThis (e.g., __morphTouchLiveKitMorphToken) is generally discouraged as it exposes the token to any script running in the same execution context. While this might be a workaround for the patched library, consider if there's a more secure way to pass this token, such as through a closure or a dedicated configuration object that isn't globally accessible.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
src/pages/advisor/VideoCalls.tsx (1)

31-31: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the centralized getLiveKitServerUrl() helper instead of hardcoding the URL.

The PR introduces getLiveKitServerUrl() in livekitConfig.ts but this file still uses a hardcoded constant with the same fallback value. This creates maintenance burden and could lead to inconsistencies if the configuration logic changes.

♻️ Proposed fix to use the helper
-const LIVEKIT_SERVER_URL = 'ws://localhost:7881';
+import { getLiveKitConnectOptions, getLiveKitServerUrl } from '../../lib/livekitConfig';
+
+const LIVEKIT_SERVER_URL = getLiveKitServerUrl();

Note: Update the import on line 26 to include getLiveKitServerUrl.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/advisor/VideoCalls.tsx` at line 31, Replace the hardcoded
LIVEKIT_SERVER_URL constant in VideoCalls.tsx with the centralized helper
getLiveKitServerUrl() from livekitConfig.ts: update the import (line previously
importing livekit config) to include getLiveKitServerUrl, remove or replace the
const LIVEKIT_SERVER_URL = 'ws://localhost:7881' and call getLiveKitServerUrl()
where the constant was used (e.g., inside any connect or URL construction logic)
so the component uses the shared configuration helper.
🧹 Nitpick comments (3)
patches/livekit-client+2.17.3.patch (1)

20-26: ⚡ Quick win

Document the tight coupling between this patch and livekitMorphToken.ts.

The patch relies on globalThis.__morphTouchLiveKitMorphToken being set by src/lib/livekitMorphToken.ts. This creates implicit coupling between the patch and application code that may not be obvious to future maintainers.

Additionally, upgrading livekit-client will silently break this authentication flow unless the patch is regenerated and tested.

Consider adding:

  1. A comment in src/lib/livekitMorphToken.ts referencing this patch file
  2. A README or doc explaining the morph token authentication architecture
  3. Testing the patch after any livekit-client version updates

Also applies to: 35-41

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/livekit-client`+2.17.3.patch around lines 20 - 26, This patch adds
conditional use of a morph token via globalThis.__morphTouchLiveKitMorphToken in
the livekit-client param handling, creating an implicit coupling to
src/lib/livekitMorphToken.ts; to fix, add a clear code comment in
src/lib/livekitMorphToken.ts referencing this patch and explaining that it must
set globalThis.__morphTouchLiveKitMorphToken for morph auth, add a short README
or docs section describing the morph token authentication flow and the
dependency on regenerating the patch when upgrading livekit-client, and include
a test or checklist step in CI to validate the patched auth behavior after any
livekit-client version bump (reference symbols:
globalThis.__morphTouchLiveKitMorphToken, livekitMorphToken.ts, and the patch
block that sets params.access_token/room_token).
src/pages/advisor/ChatManagement.tsx (1)

130-141: 💤 Low value

Minor: fromAttrs typed as string but expression can short-circuit to a boolean-ish empty string.

The chained || returns the first truthy operand or the final operand. Because each branch evaluates to either a non-empty trimmed string or false/'', fromAttrs ends up string when truthy and '' (or false-shaped at the type level via string | false) when not. The runtime behavior is correct, but the TypeScript inference is a bit awkward; using explicit guards is clearer and avoids surprises if a future attribute returns a non-string truthy value:

♻️ Proposed refactor
 function getRezervationIdForVideoCall(room: ChatRoomInstance): string | null {
   const a = room.attributes;
-  const fromAttrs =
-    (typeof a?.randevuKey === 'string' && a.randevuKey.trim()) ||
-    (typeof a?.rezervationKey === 'string' && a.rezervationKey.trim()) ||
-    '';
-  if (fromAttrs) return fromAttrs;
+  const randevu = typeof a?.randevuKey === 'string' ? a.randevuKey.trim() : '';
+  if (randevu) return randevu;
+  const rezervation = typeof a?.rezervationKey === 'string' ? a.rezervationKey.trim() : '';
+  if (rezervation) return rezervation;
   if (a?.roomType === 'rezervation' && room.id && room.id.trim()) return room.id.trim();
   if (a?.roomType === 'rezervation' && room.key && room.key.trim()) return room.key.trim();
   return null;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/advisor/ChatManagement.tsx` around lines 130 - 141, The variable
fromAttrs in getRezervationIdForVideoCall can be inferred as a non-string
(''/false) due to the chained || expression; update the function to explicitly
guard and normalize attribute values: check each attribute (a.randevuKey and
a.rezervationKey) with typeof === 'string' and const trimmed = attr.trim(),
return the first non-empty trimmed string (or set fromAttrs to null) so
TypeScript sees a consistent string|null, and then fall back to room.id/room.key
checks; reference getRezervationIdForVideoCall, fromAttrs, and room.attributes
when making the change.
src/pages/advisor/VideoCall.tsx (1)

227-371: 💤 Low value

Polling timeout is ~150s with no exponential backoff or user feedback on attempt count.

pollAttemptsRef.current > 50 with a fixed 3s interval gives a roughly 2.5-minute timeout. A few small improvements:

  • The "preparing call" screen at lines 388–409 gives no signal that the system is still polling vs. stalled; consider showing a counter or progress indicator after, e.g., 5 attempts so users know to wait or cancel.
  • Network failures (catch {} at line 359) and !res.ok (line 310) consume an attempt and immediately reschedule the next poll, which is reasonable, but consider a small jitter to avoid synchronized retries from multiple clients hammering the same reservation.

These are UX/operational refinements, not correctness issues — feel free to defer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/advisor/VideoCall.tsx` around lines 227 - 371, The poll loop in
VideoCall uses a fixed 3s retry and a hard cap via pollAttemptsRef.current > 50
which yields ~150–180s timeout and no user feedback; update the polling to use
exponential backoff with jitter in schedule (e.g., baseDelay = 3000, multiply by
1.5–2 up to a maxDelay) and add random jitter before setTimeout to avoid
thundering herd, keep pollAttemptsRef.current but only increment it on
meaningful failures (e.g., after a full poll iteration, not on minor transient
checks), and expose the attempt count in component state (e.g., a small attempts
state tied to pollAttemptsRef) so the preparing-call UI (phase === 'poll') can
surface a retry counter or progress indicator after N attempts (like 5) and
allow a cancel action that calls goDashboard/handleCallFailure; update
tryFetchTokenViaRoom and error catch paths to use the same schedule/backoff
helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@patches/livekit-client`+2.17.3.patch:
- Around line 1-44: The project has a patch file
(patches/livekit-client+2.17.3.patch) but no postinstall hook to apply it;
update package.json to add a postinstall script that runs patch-package so the
livekit-client morph token changes are applied automatically after npm/yarn
install and ensure the patch-package dependency is listed in
dependencies/devDependencies; target the package.json "scripts" section and add
a postinstall entry to run patch-package.

In `@src/lib/livekitConfig.ts`:
- Around line 16-21: The isValidTurnServer validator currently accepts empty
strings as urls; update isValidTurnServer to ensure that when server.urls is a
string it is non-empty (trimmed length > 0) and when it is an array every
element is a non-empty string (each string trimmed length > 0) so empty or
whitespace-only URL entries are rejected; adjust the checks in the
isValidTurnServer function (referenced by name) to validate non-empty URL values
before returning true.

In `@src/lib/livekitMorphToken.ts`:
- Around line 1-7: The current implementation mutates globalThis via
MorphLiveKitGlobal and setLiveKitMorphVideoToken which risks token leakage;
replace the global mutable approach with a React Context: create a
LiveKitMorphTokenContext provider that holds the token state and exposes a
setter (e.g., useLiveKitMorphToken hook), update consumers (VideoCall
components) to read/set the token from that context instead of calling
setLiveKitMorphVideoToken/globalThis, and remove or deprecate the global
setLiveKitMorphVideoToken export so token lifetime is encapsulated by React
component tree and cleanup occurs via context/provider unmounting.

In `@src/lib/unwrapMorphTouchInstance.ts`:
- Around line 5-15: The function unwrapMorphTouchInstance can infinitely recurse
on circular structures (e.g., o.instance === o); modify unwrapMorphTouchInstance
to track visited objects (use a WeakSet) and check the set before descending
into recursion: mark the current object as visited when first seen and return
payload immediately if an object is already in the visited set; apply this check
before recursing on o.instance, o.data, and o.items[0] and ensure primitive/NULL
short-circuits remain unchanged. Use the existing symbols
(unwrapMorphTouchInstance, payload, o, attributes, videoCallUrls,
webrtcIntegration, instance, data, items) so the fix is localized and preserves
current unwrapping logic while preventing infinite recursion.

In `@src/pages/advisor/VideoCall.tsx`:
- Around line 46-96: The advisor and customer video call pages duplicate lots of
UI and logic; refactor by creating a single parameterized component (e.g.,
VideoCallPage) that accepts props like { participantId, participantRole,
remoteLabel, dashboardPath } and move shared components/logic into it: extract
AdvisorCallControls/CustomerVideoControls into a single CallControls (keeping
toggleBlur and useLocalParticipant usage), combine
RemoteCustomerStage/RemoteAdvisorStage into a RemoteStage, unify
LocalCameraPreview and ActiveAdvisorCall/ActiveCustomerCall into shared
components, and extract the polling/token logic into a hook named
useLiveKitReservationConnection used by VideoCallPage; keep
src/pages/advisor/VideoCall.tsx and src/pages/customer/VideoCall.tsx as thin
re-exports that pass ADVISOR_ID/customerId and labels to the shared
VideoCallPage.
- Around line 50-69: Guard against a missing localParticipant and avoid the
unsafe cast in toggleBlur: early-return if localParticipant is falsy before
calling localParticipant.getTrackPublication(Track.Source.Camera), and verify
the returned track is actually a LocalVideoTrack (use an instanceof/type check
rather than "as LocalVideoTrack") before calling stopProcessor/setProcessor;
also change setBlurEnabled(!blurEnabled) to the functional updater
setBlurEnabled(v => !v) to prevent stale state when the async processor
operations (stopProcessor/setProcessor) complete. Ensure
supportsBackgroundProcessors() check and toast error handling remain unchanged.

In `@src/pages/customer/Dashboard.tsx`:
- Around line 475-484: The customer-side "soon" window uses thirtyMin (30 * 60 *
1000) and produces upcomingSoon from activeReservations, which conflicts with
the advisor's FIFTEEN_MIN_MS (15 * 60 * 1000) and produces UI/behavior
mismatches; change thirtyMin to match the advisor constant (15 minutes) and
update the empty-state/helper text ("30 dakika içinde başlayacak randevu yok"
and any "30 dk" copy) to "15 dakika" so both Dashboard.tsx (variables: now,
thirtyMin, upcomingSoon, activeReservations.filter) and Appointments.tsx helper
text remain consistent with FIFTEEN_MIN_MS and
canStartVideoMeet/splitStartersAndOther behavior.

---

Outside diff comments:
In `@src/pages/advisor/VideoCalls.tsx`:
- Line 31: Replace the hardcoded LIVEKIT_SERVER_URL constant in VideoCalls.tsx
with the centralized helper getLiveKitServerUrl() from livekitConfig.ts: update
the import (line previously importing livekit config) to include
getLiveKitServerUrl, remove or replace the const LIVEKIT_SERVER_URL =
'ws://localhost:7881' and call getLiveKitServerUrl() where the constant was used
(e.g., inside any connect or URL construction logic) so the component uses the
shared configuration helper.

---

Nitpick comments:
In `@patches/livekit-client`+2.17.3.patch:
- Around line 20-26: This patch adds conditional use of a morph token via
globalThis.__morphTouchLiveKitMorphToken in the livekit-client param handling,
creating an implicit coupling to src/lib/livekitMorphToken.ts; to fix, add a
clear code comment in src/lib/livekitMorphToken.ts referencing this patch and
explaining that it must set globalThis.__morphTouchLiveKitMorphToken for morph
auth, add a short README or docs section describing the morph token
authentication flow and the dependency on regenerating the patch when upgrading
livekit-client, and include a test or checklist step in CI to validate the
patched auth behavior after any livekit-client version bump (reference symbols:
globalThis.__morphTouchLiveKitMorphToken, livekitMorphToken.ts, and the patch
block that sets params.access_token/room_token).

In `@src/pages/advisor/ChatManagement.tsx`:
- Around line 130-141: The variable fromAttrs in getRezervationIdForVideoCall
can be inferred as a non-string (''/false) due to the chained || expression;
update the function to explicitly guard and normalize attribute values: check
each attribute (a.randevuKey and a.rezervationKey) with typeof === 'string' and
const trimmed = attr.trim(), return the first non-empty trimmed string (or set
fromAttrs to null) so TypeScript sees a consistent string|null, and then fall
back to room.id/room.key checks; reference getRezervationIdForVideoCall,
fromAttrs, and room.attributes when making the change.

In `@src/pages/advisor/VideoCall.tsx`:
- Around line 227-371: The poll loop in VideoCall uses a fixed 3s retry and a
hard cap via pollAttemptsRef.current > 50 which yields ~150–180s timeout and no
user feedback; update the polling to use exponential backoff with jitter in
schedule (e.g., baseDelay = 3000, multiply by 1.5–2 up to a maxDelay) and add
random jitter before setTimeout to avoid thundering herd, keep
pollAttemptsRef.current but only increment it on meaningful failures (e.g.,
after a full poll iteration, not on minor transient checks), and expose the
attempt count in component state (e.g., a small attempts state tied to
pollAttemptsRef) so the preparing-call UI (phase === 'poll') can surface a retry
counter or progress indicator after N attempts (like 5) and allow a cancel
action that calls goDashboard/handleCallFailure; update tryFetchTokenViaRoom and
error catch paths to use the same schedule/backoff helper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7dd127a7-cdd8-44b0-9fef-d7c896be5efe

📥 Commits

Reviewing files that changed from the base of the PR and between c54f235 and ee80851.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (18)
  • package.json
  • patches/livekit-client+2.17.3.patch
  • src/App.tsx
  • src/lib/api.ts
  • src/lib/livekitConfig.ts
  • src/lib/livekitMorphToken.ts
  • src/lib/resolveLiveKitFromVideoCallUrl.ts
  • src/lib/rezervationVideoToken.ts
  • src/lib/unwrapMorphTouchInstance.ts
  • src/pages/RoleSelect.tsx
  • src/pages/advisor/Appointments.tsx
  • src/pages/advisor/ChatManagement.tsx
  • src/pages/advisor/VideoCall.tsx
  • src/pages/advisor/VideoCalls.tsx
  • src/pages/advisor/index.ts
  • src/pages/customer/Dashboard.tsx
  • src/pages/customer/VideoCall.tsx
  • src/pages/customer/index.ts

Comment on lines +1 to +44
diff --git a/node_modules/livekit-client/dist/livekit-client.esm.mjs b/node_modules/livekit-client/dist/livekit-client.esm.mjs
index d5fb341..e7a92ca 100644
--- a/node_modules/livekit-client/dist/livekit-client.esm.mjs
+++ b/node_modules/livekit-client/dist/livekit-client.esm.mjs
@@ -15046,6 +15046,9 @@ class SignalClient {
if (redactedUrl.searchParams.has('access_token')) {
redactedUrl.searchParams.set('access_token', '<redacted>');
}
+ if (redactedUrl.searchParams.has('room_token')) {
+ redactedUrl.searchParams.set('room_token', '<redacted>');
+ }
this.log.debug("connecting to ".concat(redactedUrl), Object.assign({
reconnect: opts.reconnect,
reconnectReason: opts.reconnectReason
@@ -15679,7 +15682,13 @@ function toProtoSessionDescription(rsd, id) {
function createConnectionParams(token, info, opts) {
var _a;
const params = new URLSearchParams();
- params.set('access_token', token);
+ var morph = typeof globalThis !== 'undefined' && globalThis.__morphTouchLiveKitMorphToken;
+ if (morph && typeof morph === 'string' && morph.length > 0) {
+ params.set('access_token', morph);
+ params.set('room_token', token);
+ } else {
+ params.set('access_token', token);
+ }
// opts
if (opts.reconnect) {
params.set('reconnect', '1');
@@ -15722,7 +15731,13 @@ function createConnectionParams(token, info, opts) {
}
function createJoinRequestConnectionParams(token, info, opts) {
const params = new URLSearchParams();
- params.set('access_token', token);
+ var morph = typeof globalThis !== 'undefined' && globalThis.__morphTouchLiveKitMorphToken;
+ if (morph && typeof morph === 'string' && morph.length > 0) {
+ params.set('access_token', morph);
+ params.set('room_token', token);
+ } else {
+ params.set('access_token', token);
+ }
const joinRequest = new JoinRequest({
clientInfo: info,
connectionSettings: new ConnectionSettings({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if postinstall script runs patch-package.

cat package.json | jq '.scripts.postinstall'

Repository: burgan-tech/morph-touch-ui

Length of output: 75


🏁 Script executed:

# Check if patch-package is in dependencies
cat package.json | jq '.dependencies, .devDependencies' | grep -A 5 -B 5 patch-package

Repository: burgan-tech/morph-touch-ui

Length of output: 347


🏁 Script executed:

# Verify patches directory and file exist
ls -la patches/ 2>/dev/null | head -20

Repository: burgan-tech/morph-touch-ui

Length of output: 258


🏁 Script executed:

# Check if patches directory exists at all
fd patches/ --type d

Repository: burgan-tech/morph-touch-ui

Length of output: 408


Add postinstall script to apply patches automatically on install.

The patch-package dependency is installed, and the patch file patches/livekit-client+2.17.3.patch exists, but the postinstall script is not configured in package.json. Without it, the morph token authentication changes will not be applied during npm install or yarn install.

Add this to package.json:

"scripts": {
  "postinstall": "patch-package"
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/livekit-client`+2.17.3.patch around lines 1 - 44, The project has a
patch file (patches/livekit-client+2.17.3.patch) but no postinstall hook to
apply it; update package.json to add a postinstall script that runs
patch-package so the livekit-client morph token changes are applied
automatically after npm/yarn install and ensure the patch-package dependency is
listed in dependencies/devDependencies; target the package.json "scripts"
section and add a postinstall entry to run patch-package.

Comment thread src/lib/livekitConfig.ts
Comment on lines +16 to +21
function isValidTurnServer(value: unknown): value is LiveKitTurnServer {
if (!value || typeof value !== 'object') return false;
const server = value as Record<string, unknown>;
const urls = server.urls;
return typeof urls === 'string' || (Array.isArray(urls) && urls.every((url) => typeof url === 'string'));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate that URL strings are non-empty.

The validation checks that urls is a string or array of strings, but doesn't verify the strings are non-empty. Empty URLs would pass validation but cause obscure connection failures later.

🛡️ Proposed fix to reject empty URLs
 function isValidTurnServer(value: unknown): value is LiveKitTurnServer {
   if (!value || typeof value !== 'object') return false;
   const server = value as Record<string, unknown>;
   const urls = server.urls;
-  return typeof urls === 'string' || (Array.isArray(urls) && urls.every((url) => typeof url === 'string'));
+  if (typeof urls === 'string') return urls.trim() !== '';
+  return Array.isArray(urls) && urls.every((url) => typeof url === 'string' && url.trim() !== '');
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/livekitConfig.ts` around lines 16 - 21, The isValidTurnServer
validator currently accepts empty strings as urls; update isValidTurnServer to
ensure that when server.urls is a string it is non-empty (trimmed length > 0)
and when it is an array every element is a non-empty string (each string trimmed
length > 0) so empty or whitespace-only URL entries are rejected; adjust the
checks in the isValidTurnServer function (referenced by name) to validate
non-empty URL values before returning true.

Comment on lines +1 to +7
type MorphLiveKitGlobal = typeof globalThis & { __morphTouchLiveKitMorphToken?: string };

/** Patched livekit-client reads this for Morph gateway auth (access_token + room_token). */
export function setLiveKitMorphVideoToken(token: string | null): void {
const g = globalThis as MorphLiveKitGlobal;
g.__morphTouchLiveKitMorphToken = token && token.length > 0 ? token : undefined;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find all usages of setLiveKitMorphVideoToken and verify cleanup.

rg -nP -A10 'setLiveKitMorphVideoToken' --type=ts --type=tsx -g '!node_modules'

Repository: burgan-tech/morph-touch-ui

Length of output: 98


🏁 Script executed:

#!/bin/bash
# Description: Find all usages of setLiveKitMorphVideoToken and verify cleanup.

# First, let's search without specifying unsupported file types
rg -nP -A10 'setLiveKitMorphVideoToken' --type=ts -g '!node_modules'

# Also search for any .tsx files using a pattern
rg -nP -A10 'setLiveKitMorphVideoToken' -g '*.tsx' -g '!node_modules'

Repository: burgan-tech/morph-touch-ui

Length of output: 6388


🏁 Script executed:

# Let's also find the file that exports this function and understand its structure
find . -name "*.ts" -o -name "*.tsx" | grep -E "(livekit|Morph)" | head -20

Repository: burgan-tech/morph-touch-ui

Length of output: 166


🏁 Script executed:

# Get a broader view of all TypeScript/TSX files in src to understand the project structure
fd --type f -e ts -e tsx src | head -30

Repository: burgan-tech/morph-touch-ui

Length of output: 52


Cleanup is correctly implemented, but consider using React Context to eliminate globalThis mutation risks.

Cleanup is properly in place—both VideoCall components (customer and advisor) call setLiveKitMorphVideoToken(null) in a useEffect cleanup function. However, relying on globalThis for shared state introduces a vulnerability: if cleanup fails (e.g., due to an error during unmount or abrupt navigation), the token persists and could leak into subsequent sessions or user contexts.

Since cleanup is already implemented and both callers use the correct pattern, the immediate risk is mitigated. That said, migrating to React Context would eliminate the dependency on global mutable state and provide better encapsulation and predictability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/livekitMorphToken.ts` around lines 1 - 7, The current implementation
mutates globalThis via MorphLiveKitGlobal and setLiveKitMorphVideoToken which
risks token leakage; replace the global mutable approach with a React Context:
create a LiveKitMorphTokenContext provider that holds the token state and
exposes a setter (e.g., useLiveKitMorphToken hook), update consumers (VideoCall
components) to read/set the token from that context instead of calling
setLiveKitMorphVideoToken/globalThis, and remove or deprecate the global
setLiveKitMorphVideoToken export so token lifetime is encapsulated by React
component tree and cleanup occurs via context/provider unmounting.

Comment on lines +5 to +15
export function unwrapMorphTouchInstance(payload: unknown): unknown {
if (payload == null || typeof payload !== 'object') return payload;
const o = payload as Record<string, unknown>;
const attrs = o.attributes;
if (attrs != null && typeof attrs === 'object') return payload;
if (o.videoCallUrls != null || o.webrtcIntegration != null) return payload;
if (o.instance != null) return unwrapMorphTouchInstance(o.instance);
if (o.data != null) return unwrapMorphTouchInstance(o.data);
if (Array.isArray(o.items) && o.items.length > 0) return unwrapMorphTouchInstance(o.items[0]);
return payload;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Add circular reference protection to prevent infinite recursion.

The recursive unwrapping does not protect against circular references. If the payload contains a cycle (e.g., o.instance === o or o.data.instance === o), the function will recurse indefinitely until the call stack limit is reached, causing a runtime exception.

🛡️ Proposed fix using a WeakSet to track visited objects
-export function unwrapMorphTouchInstance(payload: unknown): unknown {
-  if (payload == null || typeof payload !== 'object') return payload;
-  const o = payload as Record<string, unknown>;
+export function unwrapMorphTouchInstance(payload: unknown, visited = new WeakSet<object>()): unknown {
+  if (payload == null || typeof payload !== 'object') return payload;
+  const o = payload as Record<string, unknown>;
+  if (visited.has(o)) return payload;
+  visited.add(o);
   const attrs = o.attributes;
   if (attrs != null && typeof attrs === 'object') return payload;
   if (o.videoCallUrls != null || o.webrtcIntegration != null) return payload;
-  if (o.instance != null) return unwrapMorphTouchInstance(o.instance);
-  if (o.data != null) return unwrapMorphTouchInstance(o.data);
-  if (Array.isArray(o.items) && o.items.length > 0) return unwrapMorphTouchInstance(o.items[0]);
+  if (o.instance != null) return unwrapMorphTouchInstance(o.instance, visited);
+  if (o.data != null) return unwrapMorphTouchInstance(o.data, visited);
+  if (Array.isArray(o.items) && o.items.length > 0) return unwrapMorphTouchInstance(o.items[0], visited);
   return payload;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/unwrapMorphTouchInstance.ts` around lines 5 - 15, The function
unwrapMorphTouchInstance can infinitely recurse on circular structures (e.g.,
o.instance === o); modify unwrapMorphTouchInstance to track visited objects (use
a WeakSet) and check the set before descending into recursion: mark the current
object as visited when first seen and return payload immediately if an object is
already in the visited set; apply this check before recursing on o.instance,
o.data, and o.items[0] and ensure primitive/NULL short-circuits remain
unchanged. Use the existing symbols (unwrapMorphTouchInstance, payload, o,
attributes, videoCallUrls, webrtcIntegration, instance, data, items) so the fix
is localized and preserves current unwrapping logic while preventing infinite
recursion.

Comment on lines +46 to +96
function AdvisorCallControls({ onLeave }: { onLeave: () => void }) {
const [blurEnabled, setBlurEnabled] = useState(false);
const { localParticipant } = useLocalParticipant();

const toggleBlur = useCallback(async () => {
if (!supportsBackgroundProcessors()) {
toast('Arka plan bulanıklaştırma bu tarayıcıda desteklenmiyor', 'error');
return;
}
try {
const camPub = localParticipant.getTrackPublication(Track.Source.Camera);
const track = camPub?.track;
if (!track) return;
const videoTrack = track as import('livekit-client').LocalVideoTrack;
if (blurEnabled) {
await videoTrack.stopProcessor();
} else {
await videoTrack.setProcessor(BackgroundBlur(10));
}
setBlurEnabled(!blurEnabled);
} catch (e) {
toast(String(e), 'error');
}
}, [blurEnabled, localParticipant]);

return (
<div className="video-controls">
<TrackToggle
source={Track.Source.Camera}
className={cn('video-controls', 'btn')}
title="Kamera"
/>
<TrackToggle
source={Track.Source.Microphone}
className={cn('video-controls', 'btn')}
title="Mikrofon"
/>
<button
type="button"
className={blurEnabled ? 'active' : 'inactive'}
onClick={toggleBlur}
title="Arka plan bulanıklaştırma"
>
<Scan size={18} />
</button>
<button type="button" className="end-call" onClick={onLeave} title="Görüşmeyi bitir">
<PhoneOff size={20} />
</button>
</div>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Significant code duplication with src/pages/customer/VideoCall.tsx.

AdvisorCallControls/CustomerVideoControls, RemoteCustomerStage/RemoteAdvisorStage, LocalCameraPreview, ActiveAdvisorCall/ActiveCustomerCall, and the entire useEffect polling pipeline are essentially identical between the two files (differing only in ADVISOR_ID vs customerId, the "danışman" vs "müşteri" label, and the dashboard route). This roughly doubles the maintenance surface for LiveKit changes, polling timeouts, error UX, and morph token handling.

Consider consolidating into a single parameterized VideoCallPage (e.g., in src/components/videocall/ or src/pages/shared/) accepting { participantId, participantRole, remoteLabel, dashboardPath } and re-exporting thin wrappers from src/pages/advisor/VideoCall.tsx and src/pages/customer/VideoCall.tsx. The polling/token-resolution logic can move to a shared hook (e.g., useLiveKitReservationConnection).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/advisor/VideoCall.tsx` around lines 46 - 96, The advisor and
customer video call pages duplicate lots of UI and logic; refactor by creating a
single parameterized component (e.g., VideoCallPage) that accepts props like {
participantId, participantRole, remoteLabel, dashboardPath } and move shared
components/logic into it: extract AdvisorCallControls/CustomerVideoControls into
a single CallControls (keeping toggleBlur and useLocalParticipant usage),
combine RemoteCustomerStage/RemoteAdvisorStage into a RemoteStage, unify
LocalCameraPreview and ActiveAdvisorCall/ActiveCustomerCall into shared
components, and extract the polling/token logic into a hook named
useLiveKitReservationConnection used by VideoCallPage; keep
src/pages/advisor/VideoCall.tsx and src/pages/customer/VideoCall.tsx as thin
re-exports that pass ADVISOR_ID/customerId and labels to the shared
VideoCallPage.

Comment on lines +50 to +69
const toggleBlur = useCallback(async () => {
if (!supportsBackgroundProcessors()) {
toast('Arka plan bulanıklaştırma bu tarayıcıda desteklenmiyor', 'error');
return;
}
try {
const camPub = localParticipant.getTrackPublication(Track.Source.Camera);
const track = camPub?.track;
if (!track) return;
const videoTrack = track as import('livekit-client').LocalVideoTrack;
if (blurEnabled) {
await videoTrack.stopProcessor();
} else {
await videoTrack.setProcessor(BackgroundBlur(10));
}
setBlurEnabled(!blurEnabled);
} catch (e) {
toast(String(e), 'error');
}
}, [blurEnabled, localParticipant]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

@livekit/components-react useLocalParticipant return value before room connection

💡 Result:

If you call useLocalParticipant before the LiveKit Room is connected, its initial return values can reflect the room’s localParticipant object “as-is” at render time (including fields like isMicrophoneEnabled / isCameraEnabled / isScreenShareEnabled), and then update after the room/local participant is created and media/participant updates start flowing. In particular: - The hook’s state is initialized from room.localParticipant on the first render: it uses React.useState(room.localParticipant) and then derives initial state for microphone/camera/screen-share booleans from that localParticipant instance. [1] - The hook then subscribes to participant media updates via observeParticipantMedia(room.localParticipant) in a useEffect, and on each update it calls setLocalParticipant(media.participant) plus updates those booleans and track publication refs. [1] So what you’re likely seeing is expected behavior for the pre-connection render cycle: useLocalParticipant returns immediately (with initial state), and only later (after the effect subscription starts and the local participant media snapshot/updates arrive) do the values become accurate for the connected/active room. Practical guidance (based on how the hook works): - Don’t assume the first render of useLocalParticipant reflects the final “connected” state; guard your UI with the connection state from LiveKit’s React hooks/components (e.g., a connection-state hook) before using the values as “ready”. (The existence of a separate connection-state hook is implied by the documented hook list.) [2] - If you need “definitely connected + local participant media ready”, wait for the room/connection readiness at the app/component level, then read useLocalParticipant values. Note: I couldn’t find a specific guarantee in the official docs wording that the hook only returns fully correct values after room connection; the strongest evidence is the implementation (initializing from room.localParticipant immediately, then updating through observeParticipantMedia). [1][2]

Citations:


🏁 Script executed:

# First, let's examine the VideoCall.tsx file to understand the full context
fd -t f "VideoCall.tsx" | head -5

Repository: burgan-tech/morph-touch-ui

Length of output: 135


🏁 Script executed:

# Look for the file in the src/pages/advisor directory
cat -n src/pages/advisor/VideoCall.tsx | head -100

Repository: burgan-tech/morph-touch-ui

Length of output: 4489


🏁 Script executed:

# Also search for other uses of useLocalParticipant in the codebase to understand typical patterns
rg "useLocalParticipant\|localParticipant\." -A 3 -B 1 src/pages/advisor/VideoCall.tsx

Repository: burgan-tech/morph-touch-ui

Length of output: 52


🏁 Script executed:

# Search for other uses of localParticipant guards or instanceof checks in the codebase
rg "if.*localParticipant|instanceof.*LocalVideoTrack" -B 2 -A 2

Repository: burgan-tech/morph-touch-ui

Length of output: 52


🏁 Script executed:

# Search for other uses of localParticipant in the repo to see if guards are standard
rg "localParticipant" src/ -B 2 -A 2 --type typescript --type tsx

Repository: burgan-tech/morph-touch-ui

Length of output: 105


🏁 Script executed:

# Check the livekit-client documentation or type definitions for LocalVideoTrack
rg "LocalVideoTrack|track as" src/ -B 2 -A 2 --type typescript --type tsx

Repository: burgan-tech/morph-touch-ui

Length of output: 105


🏁 Script executed:

# Search for uses of BackgroundBlur processor to see patterns
rg "BackgroundBlur|setProcessor|stopProcessor" src/ -B 3 -A 3 --type typescript --type tsx

Repository: burgan-tech/morph-touch-ui

Length of output: 105


🏁 Script executed:

# Check if there's a connection state being used elsewhere
rg "useConnection|isConnected|roomState" src/ -B 1 -A 1 --type typescript --type tsx

Repository: burgan-tech/morph-touch-ui

Length of output: 105


🏁 Script executed:

# Search for localParticipant usage in the codebase
rg "localParticipant" src/ -B 2 -A 2

Repository: burgan-tech/morph-touch-ui

Length of output: 2979


🏁 Script executed:

# Look at the customer VideoCall.tsx for comparison
cat -n src/pages/customer/VideoCall.tsx | grep -A 30 "toggleBlur\|localParticipant"

Repository: burgan-tech/morph-touch-ui

Length of output: 2667


🏁 Script executed:

# Check if there are any null checks on localParticipant in similar context
rg "if.*localParticipant\|!localParticipant" src/

Repository: burgan-tech/morph-touch-ui

Length of output: 52


🏁 Script executed:

# Search for instanceof checks on tracks or similar patterns
rg "instanceof|as.*LocalVideoTrack" src/

Repository: burgan-tech/morph-touch-ui

Length of output: 1079


Guard localParticipant and use a functional state update for blurEnabled.

Two related concerns inside toggleBlur:

  • useLocalParticipant() can return a placeholder before the room is connected; calling localParticipant.getTrackPublication(...) without checking it risks a runtime error if the user clicks the blur button early. A defensive if (!localParticipant) return; plus an instanceof check on the returned track is safer than the as LocalVideoTrack cast.
  • setBlurEnabled(!blurEnabled) reads stale state if the callback is invoked concurrently. Prefer setBlurEnabled((v) => !v) to avoid state drift if multiple rapid clicks occur during the async processor toggle.
🛡️ Proposed fix
   const toggleBlur = useCallback(async () => {
     if (!supportsBackgroundProcessors()) {
       toast('Arka plan bulanıklaştırma bu tarayıcıda desteklenmiyor', 'error');
       return;
     }
     try {
+      if (!localParticipant) return;
       const camPub = localParticipant.getTrackPublication(Track.Source.Camera);
       const track = camPub?.track;
       if (!track) return;
-      const videoTrack = track as import('livekit-client').LocalVideoTrack;
+      const { LocalVideoTrack } = await import('livekit-client');
+      if (!(track instanceof LocalVideoTrack)) return;
+      const videoTrack = track;
       if (blurEnabled) {
         await videoTrack.stopProcessor();
       } else {
         await videoTrack.setProcessor(BackgroundBlur(10));
       }
-      setBlurEnabled(!blurEnabled);
+      setBlurEnabled((v) => !v);
     } catch (e) {
       toast(String(e), 'error');
     }
   }, [blurEnabled, localParticipant]);
📝 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
const toggleBlur = useCallback(async () => {
if (!supportsBackgroundProcessors()) {
toast('Arka plan bulanıklaştırma bu tarayıcıda desteklenmiyor', 'error');
return;
}
try {
const camPub = localParticipant.getTrackPublication(Track.Source.Camera);
const track = camPub?.track;
if (!track) return;
const videoTrack = track as import('livekit-client').LocalVideoTrack;
if (blurEnabled) {
await videoTrack.stopProcessor();
} else {
await videoTrack.setProcessor(BackgroundBlur(10));
}
setBlurEnabled(!blurEnabled);
} catch (e) {
toast(String(e), 'error');
}
}, [blurEnabled, localParticipant]);
const toggleBlur = useCallback(async () => {
if (!supportsBackgroundProcessors()) {
toast('Arka plan bulanıklaştırma bu tarayıcıda desteklenmiyor', 'error');
return;
}
try {
if (!localParticipant) return;
const camPub = localParticipant.getTrackPublication(Track.Source.Camera);
const track = camPub?.track;
if (!track) return;
const { LocalVideoTrack } = await import('livekit-client');
if (!(track instanceof LocalVideoTrack)) return;
const videoTrack = track;
if (blurEnabled) {
await videoTrack.stopProcessor();
} else {
await videoTrack.setProcessor(BackgroundBlur(10));
}
setBlurEnabled((v) => !v);
} catch (e) {
toast(String(e), 'error');
}
}, [blurEnabled, localParticipant]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/advisor/VideoCall.tsx` around lines 50 - 69, Guard against a
missing localParticipant and avoid the unsafe cast in toggleBlur: early-return
if localParticipant is falsy before calling
localParticipant.getTrackPublication(Track.Source.Camera), and verify the
returned track is actually a LocalVideoTrack (use an instanceof/type check
rather than "as LocalVideoTrack") before calling stopProcessor/setProcessor;
also change setBlurEnabled(!blurEnabled) to the functional updater
setBlurEnabled(v => !v) to prevent stale state when the async processor
operations (stopProcessor/setProcessor) complete. Ensure
supportsBackgroundProcessors() check and toast error handling remain unchanged.

Comment on lines 475 to 484
const now = Date.now();
const fifteenMin = 15 * 60 * 1000;
const thirtyMin = 30 * 60 * 1000;
const upcomingSoon = activeReservations.filter((r) => {
const start = r.attributes?.startDateTime;
const end = r.attributes?.endDateTime;
if (!start || !end) return false;
const startTs = new Date(start).getTime();
const endTs = new Date(end).getTime();
return now >= startTs - fifteenMin && now <= endTs;
return now >= startTs - thirtyMin && now <= endTs;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Customer-side 30-minute "soon" window is inconsistent with advisor's 15-minute window.

thirtyMin = 30 * 60 * 1000 (and the matching empty-state message "30 dakika içinde başlayacak randevu yok" at line 896) widens the customer-side preview window. However, src/pages/advisor/Appointments.tsx still uses FIFTEEN_MIN_MS = 15 * 60 * 1000 (line 68) and splitStartersAndOther/canStartVideoMeet only surface the "Görüntülü görüşme başlat" button to the advisor within 15 minutes of the start time.

Net effect: customers can press "Görüntülü görüşmeye başla" up to 30 min in advance, but the advisor has no UI to start their side until 15 min before — those 15 minutes of customer-initiated polling will hang on the VideoCall page until they time out (or until the advisor opens the appointment via a chat room). Please align both sides on the same window (and the helper text in Appointments.tsx line 436 which still says "en fazla 15 dk kala").

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/customer/Dashboard.tsx` around lines 475 - 484, The customer-side
"soon" window uses thirtyMin (30 * 60 * 1000) and produces upcomingSoon from
activeReservations, which conflicts with the advisor's FIFTEEN_MIN_MS (15 * 60 *
1000) and produces UI/behavior mismatches; change thirtyMin to match the advisor
constant (15 minutes) and update the empty-state/helper text ("30 dakika içinde
başlayacak randevu yok" and any "30 dk" copy) to "15 dakika" so both
Dashboard.tsx (variables: now, thirtyMin, upcomingSoon,
activeReservations.filter) and Appointments.tsx helper text remain consistent
with FIFTEEN_MIN_MS and canStartVideoMeet/splitStartersAndOther behavior.

@mokoker
mokoker merged commit 5ae3210 into master May 13, 2026
6 of 7 checks passed
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.

2 participants