Enhance video call functionality and update dependencies - #3
Conversation
- 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.
Reviewer's GuideImplements 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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
📝 WalkthroughWalkthroughThis 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. ChangesLiveKit Video Calling
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
VideoCallcomponents share a lot of almost identical logic (polling rezervation, resolving LiveKit URL/token, rendering controls/stage); consider extracting shared hooks/utilities (e.g. a genericuseRezervationVideoCall+ shared call layout components) to reduce duplication and keep behavior changes in one place. - The polling logic in both
VideoCallcomponents uses multiple mutable refs (cancelledRef,timerRef,pollAttemptsRef,videoAuthRef) and inlineschedule/polldefinitions; extracting this into a reusable helper or hook with clearer state (e.g. usinguseRef+ 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>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 } }; | ||
| } |
There was a problem hiding this comment.
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:
- Import and use
useRezervationLiveKitCallin the customerVideoCall(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). - Move the shared polling/token-resolution/LiveKit wiring and error/phase handling logic from both advisor and customer
VideoCallcomponents intouseRezervationLiveKitCall:- Accept any role-specific parameters via
UseRezervationLiveKitCallOptions(e.g., headers, TURN config, retry limits). - Implement the shared
useEffectfor 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
VideoCallcomponents.
- Accept any role-specific parameters via
- Optionally extract the shared “active call” layout (e.g., the
<LiveKitRoom>wrapper and the stage/controls layout) into a reusableRezervationActiveCallcomponent used by both advisor and customer flows, parameterized by role-specific subcomponents (such asAdvisorCallControlsvsCustomerCallControls).
| 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 } : {}), | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
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.
| 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}` |
There was a problem hiding this comment.
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}` |
There was a problem hiding this comment.
security (javascript.lang.security.detect-insecure-websocket): Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.
Source: opengrep
There was a problem hiding this comment.
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.
| 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| + 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); | ||
| + } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 winUse the centralized
getLiveKitServerUrl()helper instead of hardcoding the URL.The PR introduces
getLiveKitServerUrl()inlivekitConfig.tsbut 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 winDocument the tight coupling between this patch and livekitMorphToken.ts.
The patch relies on
globalThis.__morphTouchLiveKitMorphTokenbeing set bysrc/lib/livekitMorphToken.ts. This creates implicit coupling between the patch and application code that may not be obvious to future maintainers.Additionally, upgrading
livekit-clientwill silently break this authentication flow unless the patch is regenerated and tested.Consider adding:
- A comment in
src/lib/livekitMorphToken.tsreferencing this patch file- A README or doc explaining the morph token authentication architecture
- Testing the patch after any
livekit-clientversion updatesAlso 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 valueMinor:
fromAttrstyped asstringbut 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 orfalse/'',fromAttrsends upstringwhen truthy and''(orfalse-shaped at the type level viastring | 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 valuePolling timeout is ~150s with no exponential backoff or user feedback on attempt count.
pollAttemptsRef.current > 50with 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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (18)
package.jsonpatches/livekit-client+2.17.3.patchsrc/App.tsxsrc/lib/api.tssrc/lib/livekitConfig.tssrc/lib/livekitMorphToken.tssrc/lib/resolveLiveKitFromVideoCallUrl.tssrc/lib/rezervationVideoToken.tssrc/lib/unwrapMorphTouchInstance.tssrc/pages/RoleSelect.tsxsrc/pages/advisor/Appointments.tsxsrc/pages/advisor/ChatManagement.tsxsrc/pages/advisor/VideoCall.tsxsrc/pages/advisor/VideoCalls.tsxsrc/pages/advisor/index.tssrc/pages/customer/Dashboard.tsxsrc/pages/customer/VideoCall.tsxsrc/pages/customer/index.ts
| 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({ |
There was a problem hiding this comment.
🧩 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-packageRepository: burgan-tech/morph-touch-ui
Length of output: 347
🏁 Script executed:
# Verify patches directory and file exist
ls -la patches/ 2>/dev/null | head -20Repository: burgan-tech/morph-touch-ui
Length of output: 258
🏁 Script executed:
# Check if patches directory exists at all
fd patches/ --type dRepository: 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.
| 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')); | ||
| } |
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 -30Repository: 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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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]); |
There was a problem hiding this comment.
🧩 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:
- 1: https://github.com/livekit/components-js/blob/main/packages/react/src/hooks/useLocalParticipant.ts
- 2: https://docs.livekit.io/reference/components/react/hook/uselocalparticipant/
🏁 Script executed:
# First, let's examine the VideoCall.tsx file to understand the full context
fd -t f "VideoCall.tsx" | head -5Repository: 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 -100Repository: 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.tsxRepository: 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 2Repository: 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 tsxRepository: 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 tsxRepository: 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 tsxRepository: 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 tsxRepository: burgan-tech/morph-touch-ui
Length of output: 105
🏁 Script executed:
# Search for localParticipant usage in the codebase
rg "localParticipant" src/ -B 2 -A 2Repository: 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; callinglocalParticipant.getTrackPublication(...)without checking it risks a runtime error if the user clicks the blur button early. A defensiveif (!localParticipant) return;plus aninstanceofcheck on the returned track is safer than theas LocalVideoTrackcast.setBlurEnabled(!blurEnabled)reads stale state if the callback is invoked concurrently. PrefersetBlurEnabled((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.
| 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.
| 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; | ||
| }); |
There was a problem hiding this comment.
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.
package.jsonandpackage-lock.jsonto includepatch-packageand other necessary dependencies.RoleSelect.tsxto streamline advisor selection and manual login processes.api.tsto allow for additional headers in instance retrieval.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:
Enhancements:
Build:
Chores:
Summary by CodeRabbit
New Features
Refactor