-
Notifications
You must be signed in to change notification settings - Fork 1.6k
feat: add realtime presence indicators for project collaboration #2954
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
47de425
f5a431b
4895c03
3e69e39
224d91b
3b096b2
3223787
c377198
6686694
31a6e14
928336b
e9cf1aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
CREATE TABLE IF NOT EXISTS public.user_presence ( | ||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), | ||
project_id uuid NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE, | ||
user_id uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, | ||
last_seen timestamp with time zone DEFAULT now() NOT NULL, | ||
is_online boolean DEFAULT true NOT NULL, | ||
created_at timestamp with time zone DEFAULT now() NOT NULL, | ||
updated_at timestamp with time zone DEFAULT now() NOT NULL, | ||
UNIQUE(project_id, user_id) | ||
); | ||
|
||
ALTER TABLE public.user_presence ENABLE ROW LEVEL SECURITY; | ||
|
||
CREATE POLICY "Users can view presence for accessible projects" ON public.user_presence | ||
FOR SELECT | ||
USING ( | ||
EXISTS ( | ||
SELECT 1 FROM public.user_projects | ||
WHERE user_projects.project_id = user_presence.project_id | ||
AND user_projects.user_id = auth.uid() | ||
) | ||
); | ||
|
||
CREATE POLICY "Users can manage their own presence" ON public.user_presence | ||
FOR ALL | ||
USING (user_id = auth.uid()) | ||
WITH CHECK (user_id = auth.uid()); | ||
|
||
CREATE OR REPLACE FUNCTION public.update_user_presence( | ||
p_project_id uuid, | ||
p_user_id uuid, | ||
p_is_online boolean DEFAULT true | ||
) | ||
RETURNS void | ||
LANGUAGE plpgsql | ||
SECURITY DEFINER | ||
AS $$ | ||
BEGIN | ||
INSERT INTO public.user_presence (project_id, user_id, is_online, last_seen, updated_at) | ||
VALUES (p_project_id, p_user_id, p_is_online, now(), now()) | ||
ON CONFLICT (project_id, user_id) | ||
DO UPDATE SET | ||
is_online = p_is_online, | ||
last_seen = now(), | ||
updated_at = now(); | ||
END; | ||
$$; | ||
|
||
CREATE OR REPLACE FUNCTION public.cleanup_offline_users() | ||
RETURNS void | ||
LANGUAGE plpgsql | ||
SECURITY DEFINER | ||
AS $$ | ||
BEGIN | ||
UPDATE public.user_presence | ||
SET is_online = false, updated_at = now() | ||
WHERE is_online = true | ||
AND last_seen < now() - interval '5 minutes'; | ||
END; | ||
$$; | ||
|
||
CREATE OR REPLACE FUNCTION public.presence_changes() | ||
RETURNS TRIGGER | ||
SECURITY DEFINER | ||
LANGUAGE plpgsql | ||
AS $$ | ||
DECLARE | ||
topic_project_id uuid; | ||
BEGIN | ||
topic_project_id := COALESCE(NEW.project_id, OLD.project_id); | ||
|
||
IF topic_project_id IS NOT NULL THEN | ||
PERFORM realtime.broadcast_changes( | ||
'presence:' || topic_project_id::text, | ||
TG_OP, | ||
TG_OP, | ||
TG_TABLE_NAME, | ||
TG_TABLE_SCHEMA, | ||
NEW, | ||
OLD | ||
); | ||
END IF; | ||
|
||
RETURN NULL; | ||
END; | ||
$$; | ||
|
||
DROP TRIGGER IF EXISTS handle_presence_changes ON public.user_presence; | ||
CREATE TRIGGER handle_presence_changes | ||
AFTER INSERT OR UPDATE OR DELETE | ||
ON public.user_presence | ||
FOR EACH ROW | ||
EXECUTE FUNCTION presence_changes(); | ||
|
||
DROP POLICY IF EXISTS "Authenticated users can receive presence broadcasts" ON "realtime"."messages"; | ||
CREATE POLICY "Authenticated users can receive presence broadcasts" | ||
ON "realtime"."messages" | ||
FOR SELECT | ||
TO authenticated | ||
USING ( | ||
CASE | ||
WHEN payload->>'table' = 'user_presence' THEN | ||
EXISTS ( | ||
SELECT 1 FROM public.user_projects | ||
WHERE user_projects.project_id = (payload->>'project_id')::uuid | ||
AND user_projects.user_id = auth.uid() | ||
) | ||
ELSE false | ||
END | ||
); |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,74 @@ | ||||||||||||||||||||||||||||||||||||||
'use client'; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
import { usePresenceManager } from '@/components/store/presence'; | ||||||||||||||||||||||||||||||||||||||
import { useEditorEngine } from '@/components/store/editor'; | ||||||||||||||||||||||||||||||||||||||
import { Avatar, AvatarFallback, AvatarImage } from '@onlook/ui/avatar'; | ||||||||||||||||||||||||||||||||||||||
import { Button } from '@onlook/ui/button'; | ||||||||||||||||||||||||||||||||||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@onlook/ui/tooltip'; | ||||||||||||||||||||||||||||||||||||||
import { observer } from 'mobx-react-lite'; | ||||||||||||||||||||||||||||||||||||||
import { useEffect } from 'react'; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
interface PresenceDisplayProps { | ||||||||||||||||||||||||||||||||||||||
className?: string; | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
export const PresenceDisplay = observer(({ className = '' }: PresenceDisplayProps) => { | ||||||||||||||||||||||||||||||||||||||
const presenceManager = usePresenceManager(); | ||||||||||||||||||||||||||||||||||||||
const editorEngine = useEditorEngine(); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
useEffect(() => { | ||||||||||||||||||||||||||||||||||||||
if (editorEngine.user && editorEngine.projectId) { | ||||||||||||||||||||||||||||||||||||||
presenceManager.setContext(editorEngine.user.id, editorEngine.projectId); | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
}, [editorEngine.user?.id, editorEngine.projectId, presenceManager]); | ||||||||||||||||||||||||||||||||||||||
Comment on lines
+15
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Avoid placing MobX store instances in effect dependency arrays. Line 23 includes Apply this diff to remove - }, [editorEngine.user?.id, editorEngine.projectId, presenceManager]);
+ }, [editorEngine.user?.id, editorEngine.projectId]); As per coding guidelines. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
if (!presenceManager.isConnected || presenceManager.otherOnlineUsers.length === 0) { | ||||||||||||||||||||||||||||||||||||||
return null; | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
const onlineUsers = presenceManager.otherOnlineUsers; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||||||||
<div className={`flex items-center gap-1 ${className}`}> | ||||||||||||||||||||||||||||||||||||||
{onlineUsers.slice(0, 3).map((user) => ( | ||||||||||||||||||||||||||||||||||||||
<Tooltip key={user.userId}> | ||||||||||||||||||||||||||||||||||||||
<TooltipTrigger asChild> | ||||||||||||||||||||||||||||||||||||||
<div className="relative"> | ||||||||||||||||||||||||||||||||||||||
<Avatar className="h-6 w-6 border-2 border-background-primary"> | ||||||||||||||||||||||||||||||||||||||
<AvatarImage src={user.avatarUrl || undefined} alt={user.displayName} /> | ||||||||||||||||||||||||||||||||||||||
<AvatarFallback className="text-xs bg-muted text-muted-foreground"> | ||||||||||||||||||||||||||||||||||||||
{user.displayName.split(' ').map(n => n[0]).join('').slice(0, 2)} | ||||||||||||||||||||||||||||||||||||||
Comment on lines
+39
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle empty displayName edge case. If Apply this diff to add a fallback: <AvatarFallback className="text-xs bg-muted text-muted-foreground">
- {user.displayName.split(' ').map(n => n[0]).join('').slice(0, 2)}
+ {user.displayName.split(' ').map(n => n[0]).join('').slice(0, 2) || '?'}
</AvatarFallback> 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential runtime error with undefined array element access. Line 40 constructs initials by accessing Apply this diff to filter out empty strings before mapping: - {user.displayName.split(' ').map(n => n[0]).join('').slice(0, 2)}
+ {user.displayName.split(' ').filter(n => n).map(n => n[0]).join('').slice(0, 2)} 🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||
</AvatarFallback> | ||||||||||||||||||||||||||||||||||||||
</Avatar> | ||||||||||||||||||||||||||||||||||||||
<div className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full bg-green-500 border-2 border-background-primary" /> | ||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||
</TooltipTrigger> | ||||||||||||||||||||||||||||||||||||||
<TooltipContent side="bottom" className="mt-1" hideArrow> | ||||||||||||||||||||||||||||||||||||||
<p className="text-sm">{user.displayName} is online</p> | ||||||||||||||||||||||||||||||||||||||
</TooltipContent> | ||||||||||||||||||||||||||||||||||||||
</Tooltip> | ||||||||||||||||||||||||||||||||||||||
))} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
{onlineUsers.length > 3 && ( | ||||||||||||||||||||||||||||||||||||||
<Tooltip> | ||||||||||||||||||||||||||||||||||||||
<TooltipTrigger asChild> | ||||||||||||||||||||||||||||||||||||||
<Button | ||||||||||||||||||||||||||||||||||||||
variant="outline" | ||||||||||||||||||||||||||||||||||||||
size="sm" | ||||||||||||||||||||||||||||||||||||||
className="h-6 w-6 rounded-full p-0 text-xs bg-muted hover:bg-muted/80" | ||||||||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||||||
+{onlineUsers.length - 3} | ||||||||||||||||||||||||||||||||||||||
</Button> | ||||||||||||||||||||||||||||||||||||||
</TooltipTrigger> | ||||||||||||||||||||||||||||||||||||||
<TooltipContent side="bottom" className="mt-1" hideArrow> | ||||||||||||||||||||||||||||||||||||||
<div className="text-sm"> | ||||||||||||||||||||||||||||||||||||||
{onlineUsers.slice(3).map(user => ( | ||||||||||||||||||||||||||||||||||||||
<div key={user.userId}>{user.displayName}</div> | ||||||||||||||||||||||||||||||||||||||
))} | ||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||
</TooltipContent> | ||||||||||||||||||||||||||||||||||||||
</Tooltip> | ||||||||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { PresenceProvider, usePresenceManager } from './provider'; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,163 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { makeAutoObservable, reaction } from 'mobx'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { api } from '@/trpc/react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { createClient } from '@/utils/supabase/client'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export interface PresenceUser { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
userId: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
displayName: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
avatarUrl?: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lastSeen: Date; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export class PresenceManager { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
onlineUsers: PresenceUser[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
currentUserId: string | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
currentProjectId: string | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
isConnected = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
isLoading = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private subscription: any = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Replace Line 20 declares Apply this diff to improve type safety: +import type { RealtimeChannel } from '@supabase/supabase-js';
+
export class PresenceManager {
// ...
- private subscription: any = null;
+ private subscription: RealtimeChannel | null = null; As per coding guidelines. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
constructor() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
makeAutoObservable(this); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
reaction( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
() => this.currentProjectId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(projectId) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (projectId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.joinProject(projectId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.leaveProject(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+25
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against concurrent join/leave operations. The reaction triggers Consider adding a guard: export class PresenceManager {
onlineUsers: PresenceUser[] = [];
currentUserId: string | null = null;
currentProjectId: string | null = null;
isConnected = false;
isLoading = false;
private subscription: any = null;
+ private pendingOperation: Promise<void> | null = null;
constructor() {
makeAutoObservable(this);
reaction(
() => this.currentProjectId,
- (projectId) => {
+ async (projectId) => {
+ await this.pendingOperation;
if (projectId) {
- this.joinProject(projectId);
+ this.pendingOperation = this.joinProject(projectId);
} else {
- this.leaveProject();
+ this.pendingOperation = this.leaveProject();
}
+ await this.pendingOperation;
+ this.pendingOperation = null;
}
);
} 🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
setContext(userId: string, projectId: string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.currentUserId = userId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.currentProjectId = projectId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async joinProject(projectId: string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!this.currentUserId) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.isLoading = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.currentProjectId = projectId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
await api.presence.joinProject.mutate({ projectId }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
await this.loadProjectPresence(projectId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.subscribeToPresenceUpdates(projectId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.isConnected = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.error('Error joining project:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.isLoading = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+45
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improve error handling and state cleanup. Issues:
Apply this diff: async joinProject(projectId: string) {
if (!this.currentUserId) return;
try {
this.isLoading = true;
- this.currentProjectId = projectId;
await api.presence.joinProject.mutate({ projectId });
await this.loadProjectPresence(projectId);
this.subscribeToPresenceUpdates(projectId);
this.isConnected = true;
} catch (error) {
console.error('Error joining project:', error);
+ this.isConnected = false;
+ this.onlineUsers = [];
+ throw error;
} finally {
this.isLoading = false;
}
} 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async leaveProject() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!this.currentProjectId || !this.currentUserId) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
await api.presence.leaveProject.mutate({ projectId: this.currentProjectId }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.unsubscribeFromPresenceUpdates(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.onlineUsers = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.currentProjectId = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.isConnected = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.error('Error leaving project:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private async loadProjectPresence(projectId: string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const presenceData = await api.presence.getProjectPresence.query({ projectId }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.onlineUsers = presenceData.map(p => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
userId: p.userId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
displayName: p.displayName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
avatarUrl: p.avatarUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lastSeen: p.lastSeen, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
})); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.error('Error loading project presence:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private subscribeToPresenceUpdates(projectId: string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.unsubscribeFromPresenceUpdates(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const supabase = createClient(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const channel = supabase | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.channel(`presence:${projectId}`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.on( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
'postgres_changes', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
event: '*', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
schema: 'public', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
table: 'user_presence', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
filter: `project_id=eq.${projectId}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(payload) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.handlePresenceUpdate(payload); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.subscribe(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.subscription = channel; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private unsubscribeFromPresenceUpdates() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (this.subscription) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.subscription.unsubscribe(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.subscription = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+114
to
+119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chainVerify if unsubscribe should be awaited. The Supabase channel 🌐 Web query:
💡 Result: Yes — RealtimeChannel.unsubscribe() returns a Promise (resolves to "ok" | "error" | "timed out") and should be awaited when you need to ensure the leave completed (e.g., in short‑lived contexts like serverless functions). [1][2] Sources:
Await Supabase channel.unsubscribe() for proper cleanup 🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private handlePresenceUpdate(payload: any) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { eventType, new: newRecord, old: oldRecord } = payload; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
switch (eventType) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
case 'INSERT': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
case 'UPDATE': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (newRecord && newRecord.is_online) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const existingIndex = this.onlineUsers.findIndex(u => u.userId === newRecord.user_id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const user: PresenceUser = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
userId: newRecord.user_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
displayName: newRecord.user?.display_name || `${newRecord.user?.first_name || ''} ${newRecord.user?.last_name || ''}`.trim(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
avatarUrl: newRecord.user?.avatar_url, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
lastSeen: new Date(newRecord.last_seen), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (existingIndex >= 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.onlineUsers[existingIndex] = user; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.onlineUsers.push(user); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.onlineUsers = this.onlineUsers.filter(u => u.userId !== newRecord?.user_id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
case 'DELETE': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.onlineUsers = this.onlineUsers.filter(u => u.userId !== oldRecord?.user_id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+121
to
+150
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Add type safety and null checks for payload handling. Issues:
Consider adding type definitions and null checks: - private handlePresenceUpdate(payload: any) {
+ private handlePresenceUpdate(payload: {
+ eventType: 'INSERT' | 'UPDATE' | 'DELETE';
+ new: any;
+ old: any;
+ }) {
const { eventType, new: newRecord, old: oldRecord } = payload;
switch (eventType) {
case 'INSERT':
case 'UPDATE':
- if (newRecord && newRecord.is_online) {
+ if (newRecord?.user && newRecord.is_online) {
const existingIndex = this.onlineUsers.findIndex(u => u.userId === newRecord.user_id);
const user: PresenceUser = {
userId: newRecord.user_id,
- displayName: newRecord.user?.display_name || `${newRecord.user?.first_name || ''} ${newRecord.user?.last_name || ''}`.trim(),
+ displayName: newRecord.user.display_name || `${newRecord.user.first_name || ''} ${newRecord.user.last_name || ''}`.trim() || 'Unknown User',
avatarUrl: newRecord.user?.avatar_url,
lastSeen: new Date(newRecord.last_seen),
}; 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chainReplace Line 121 declares Define an explicit type or interface for the expected payload structure: +interface PresencePayload {
+ eventType: 'INSERT' | 'UPDATE' | 'DELETE';
+ new?: {
+ user_id: string;
+ is_online: boolean;
+ last_seen: string;
+ user?: {
+ display_name?: string;
+ first_name?: string;
+ last_name?: string;
+ avatar_url?: string | null;
+ };
+ };
+ old?: {
+ user_id: string;
+ };
+}
+
- private handlePresenceUpdate(payload: any) {
+ private handlePresenceUpdate(payload: PresencePayload) {
const { eventType, new: newRecord, old: oldRecord } = payload; Additionally, verify that the Supabase realtime payload always includes the As per coding guidelines. 🏁 Script executed: #!/bin/bash
# Check if the user_presence trigger/function includes user relation
rg -A 10 'CREATE.*FUNCTION.*presence' apps/backend/supabase/migrations/ Length of output: 1782 🏁 Script executed: #!/bin/bash
rg -n -C3 'CREATE (TABLE|VIEW).*user_presence' apps/backend/supabase/migrations/ Length of output: 484 Add explicit 🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
get otherOnlineUsers(): PresenceUser[] { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return this.onlineUsers.filter(u => u.userId !== this.currentUserId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
isUserOnline(userId: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return this.onlineUsers.some(u => u.userId === userId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
dispose() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.leaveProject(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Ensure
cleanup_offline_users
is invoked periodically.The
cleanup_offline_users
function marks users as offline after 5 minutes of inactivity, but there's no scheduler or cron job configured in this migration to call it periodically. Without scheduled execution, stale presence records will persist indefinitely.Verify that a scheduler (e.g.,
pg_cron
, Supabase Edge Function, or external cron) is configured to call this function. If not, consider adding a scheduled job:🏁 Script executed:
Length of output: 527
Schedule
cleanup_offline_users
to run periodically. No invocation was found; add a pg_cron job, Supabase scheduled function, or equivalent to call this function every 5 minutes.🤖 Prompt for AI Agents