Skip to content
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

Collaborative Sessions - Actually create a session [Frontend] #356

Open
wants to merge 22 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/@types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,18 @@ export type Student = {
mecNumber: number
}

export type Participant = {
client_id: string
name: string
}

// TODO(Process-ing): Maybe join Student and Participant into a single type

export type CollabSession = {
id: number
id: string
name: string
lastEdited: string
lifeSpan: number
currentUser: string
lastEdited: number
expirationTime: number
link: string
participants: Array<string>
participants: Array<Participant>
}
78 changes: 68 additions & 10 deletions src/api/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class OptionalSocket {
this.socket = null;
}

is_set() {
return this.socket !== null
}

use<T>(callback: (socket: Socket) => T): T {
if (!this.socket) {
throw new Error('Socket is not connected');
Expand All @@ -29,36 +33,90 @@ class OptionalSocket {
class SessionsSocket {
private url: string;
private socket: OptionalSocket;
private _clientId: string | null;
private _sessionId: string | null;
private _sessionInfo: any;

constructor(url: string) {
this.url = url;
this.socket = new OptionalSocket();
this._clientId = null;
this._sessionId = null;
this._sessionInfo = null;
}

get sessionId(): string | null {
return this._sessionId;
}

get clientId(): string | null {
return this._clientId;
}

set sessionId(sessionId: string | null) {
this._sessionId = sessionId;
}

get sessionInfo() {
return this._sessionInfo;
}

connect() {
const newSocket = io(this.url, {
auth: {
token: 'dummy', // TODO: Replace with actual federated authentication token
}
isConnected() {
this.socket.is_set();
}

async connect(participantName: string): Promise<SessionsSocket> {
return new Promise((resolve, reject) => {
const query = {
...(this.sessionId ? { session_id: this.sessionId } : {}),
participant_name: participantName,
};

const newSocket = io(this.url, {
query,
auth: {
token: 'dummy', // TODO: Replace with actual federated authentication token
},
});

this.socket.set(newSocket);

newSocket.on('connected', data => {
this._clientId = data['client_id'];
this._sessionId = data['session_id'];
this._sessionInfo = data['session_info'];
resolve(this);
});

newSocket.on('connect_error', (err) => {
this.socket.unset();
reject(err);
});
});
this.socket.set(newSocket);
}

disconnect() {
this.socket.use(socket => socket.disconnect());
this.socket.unset();
async disconnect(): Promise<void> {
return new Promise(resolve => {
this.socket.use(socket => socket.disconnect());
this.socket.unset();
resolve();
});
}

on(event: string, callback: (...args: any[]) => void) {
this.socket.use(socket => socket.on(event, callback));
}

onAny(callback: (event: string, ...args: any[]) => void) {
this.socket.use(socket => socket.onAny(callback));
}

off(event: string, callback?: (...args: any[]) => void) {
this.socket.use(socket => socket.off(event, callback));
}

emit(event: string, ...args: any[]) {
this.socket.use(socket => socket.emit(event, args));
this.socket.use(socket => socket.emit(event, ...args));
}
}

Expand Down
167 changes: 120 additions & 47 deletions src/components/planner/sidebar/sessionController/CollabModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { XMarkIcon } from '@heroicons/react/24/solid';
import CollabPickSession from './CollabPickSession';
import CollabSessionModal from './CollabSessionModal';
import CollabSessionContext from '../../../../contexts/CollabSessionContext';
import { sessionsSocket } from '../../../../api/socket';
import { toast } from '../../../ui/use-toast';
import { useSearchParams } from 'react-router-dom';

const PICK_SESSION = 'PICK_SESSION';
const SESSION = 'SESSION';
const generateUniqueId = () => Date.now();

type Props = {
Expand All @@ -15,69 +16,141 @@ type Props = {
}

const CollabModal = ({ isOpen, closeModal }: Props) => {
const { sessions, setSessions, currentSessionId, setcurrentSessionId } = useContext(CollabSessionContext);
const [currentView, setCurrentView] = useState(PICK_SESSION); //Defines in which modal we are
const { sessions, setSessions, currentSessionId, setCurrentSessionId } = useContext(CollabSessionContext);
const currentSession = sessions.find(s => s.id === currentSessionId) || null;
const [searchParams, ] = useSearchParams();

useEffect(() => {
if (isOpen) {
if (currentSessionId !== null && sessions.find(s => s.id === currentSessionId)) {
setCurrentView(SESSION);
} else {
setCurrentView(PICK_SESSION);
}
if (searchParams.has('session')) {
const sessionId = searchParams.get('session')!;
handleStartSession(sessionId);
}
}, [isOpen, currentSessionId, sessions]);
}, []);

const currentSession = sessions.find(s => s.id === currentSessionId) || null;
// TODO: Remove this
const [interval, setInt] = useState<number | null>(null);
const [uid, ] = useState(generateUniqueId());
useEffect(() => {
if (!currentSessionId) {
if (interval)
clearInterval(interval!);
setInt(null);
return;
}

sessionsSocket.on('ping', data => {
// eslint-disable-next-line no-console
console.log('Received ping', data['id']);
});

setInt(setInterval(() => {
sessionsSocket.emit('ping', { id: uid });
// eslint-disable-next-line no-console
console.log('Sent ping', uid);
}, 1000));
}, [currentSessionId]);


const updatedSession = (sessionId: string, sessionInfo: any) => {
setSessions(prevSessions =>
prevSessions.map(session =>
session.id === sessionId ? { ...session, participants: sessionInfo['participants'] } : session
)
);
}

const handleUnexpectedDisconnect = () => {
setCurrentSessionId(null);
toast({ title: 'Foste desconectado inesperadamente', description: 'Por favor, tenta novamente mais tarde.' });
};

const addSocketListeners = socket => {
socket.on('disconnect', handleUnexpectedDisconnect);
socket.on('update_session_info', (data) => updatedSession(data['session_id'], data['session_info']));
};


const handleStartSession = (sessionId) => {
setcurrentSessionId(sessionId);
setCurrentView(SESSION);
sessionsSocket.sessionId = sessionId;
sessionsSocket.connect('TheCreator')
.then(sessionsSocket => {
if (sessions.find(session => session.id === sessionsSocket.sessionId) === undefined) {
const newSession = {
id: sessionsSocket.sessionId,
name: Math.random().toString(36).substr(2, 9),
lastEdited: Date.now(),
expirationTime: new Date(sessionsSocket.sessionInfo['expiration_time']).getTime(),
currentUser: 'TheCreator',
link: `http://localhost:3100/planner?session=${sessionsSocket.sessionId}`,
participants: sessionsSocket.sessionInfo['participants'],
}

setSessions(prevSessions => [...prevSessions, newSession]);
}

addSocketListeners(sessionsSocket);
setCurrentSessionId(sessionsSocket.sessionId);

toast({ title: 'Entrou na sessão', description: 'Convida mais amigos para se juntarem!'});
})
.catch(() => toast({ title: 'Erro ao entrar na sessão', description: 'Tente novamente mais tarde.' }));
};

const handleCreateSession = () => { //Dummy function to create a session...
const newSession = {
id: generateUniqueId(),
name: Math.random().toString(36).substr(2, 9),
lastEdited: new Date().toLocaleDateString(),
lifeSpan: 30,
currentUser: 'TheCreator',
link: `https://collab.app/session/${Date.now().toString()}`,
participants: ['TheCreator'],
};
setSessions(prevSessions => [...prevSessions, newSession]);
setcurrentSessionId(newSession.id);
setCurrentView(SESSION);
sessionsSocket.sessionId = null;
sessionsSocket.connect('TheCreator')
.then(sessionsSocket => {
const newSession = {
id: sessionsSocket.sessionId,
name: Math.random().toString(36).substr(2, 9),
lastEdited: Date.now(),
expirationTime: sessionsSocket.sessionInfo['expiration_time'],
link: `http://localhost:3100/planner?session=${sessionsSocket.sessionId}`,
participants: sessionsSocket.sessionInfo['participants'],
};

addSocketListeners(sessionsSocket);
setCurrentSessionId(sessionsSocket.sessionId);
setSessions(prevSessions => [...prevSessions, newSession]);

toast({ title: 'Sessão criada', description: 'Convida mais amigos para se juntarem!'});
})
.catch(() => toast({ title: 'Erro ao criar a sessão', description: 'Tente novamente mais tarde.' }));
};

const handleExitSession = () => {
setcurrentSessionId(null);
setCurrentView(PICK_SESSION);
sessionsSocket.off('disconnect', handleUnexpectedDisconnect);
sessionsSocket.disconnect();
toast({ title: 'Sessão abandonada', description: 'Podes voltar a ela mais tarde, ou iniciar/entrar noutra sessão.'});
setCurrentSessionId(null);
};

const handleDeleteSession = (sessionId: number | null) => {
const handleDeleteSession = (sessionId: string | null) => {
setSessions(prevSessions => prevSessions.filter(session => session.id !== sessionId));
if (currentSession?.id === sessionId) {
handleExitSession();
}
};

const handleUpdateUser = (updatedUser: string) => {
if (currentSession) {
const updatedSession = {
...currentSession,
currentUser: updatedUser,
participants: currentSession.participants.map(participant =>
participant === currentSession.currentUser ? updatedUser : participant
)
};
setSessions(prevSessions =>
prevSessions.map(session =>
session.id === currentSession.id ? updatedSession : session
)
);
}
const handleUpdateUser = (updatedName: string) => {
if (!currentSession)
return;

const updatedSession = {
...currentSession,
participants: currentSession.participants.map(participant =>
participant.client_id === sessionsSocket.clientId ? { ...participant, name: updatedName } : participant
)
};
setSessions(prevSessions =>
prevSessions.map(session =>
session.id === currentSession.id ? updatedSession : session
)
);

sessionsSocket.emit('update_participant', { 'name': updatedName });
};

return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}>
Expand All @@ -104,7 +177,7 @@ const CollabModal = ({ isOpen, closeModal }: Props) => {
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Panel className="w-full max-w-2xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<div className="flex justify-end">
<button
type="button"
Expand All @@ -115,7 +188,7 @@ const CollabModal = ({ isOpen, closeModal }: Props) => {
</button>
</div>

{currentView === PICK_SESSION && (
{currentSessionId === null && (
<CollabPickSession
sessions={sessions}
onStartSession={handleStartSession}
Expand All @@ -124,7 +197,7 @@ const CollabModal = ({ isOpen, closeModal }: Props) => {
/>
)}

{currentView === SESSION && currentSession && (
{currentSessionId !== null && (
<CollabSessionModal
session={currentSession}
onExitSession={handleExitSession}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from 'react';
import { PlayCircleIcon, UserGroupIcon } from '@heroicons/react/20/solid';
import { Button } from '../../../ui/button';
import { CollabSession } from '../../../../@types';
import toHumanReadableTimeDiff from '../../../../utils/human-time';

type Props = {
sessions: Array<CollabSession>,
onStartSession: (arg: number | null) => void
onStartSession: (arg: string | null) => void
onCreateSession: () => void
onDeleteSession: (arg: number | null) => void
onDeleteSession: (arg: string | null) => void
}

const CollabPickSession = ({ sessions, onStartSession, onCreateSession, onDeleteSession }: Props) => (
Expand Down Expand Up @@ -44,8 +44,8 @@ const CollabPickSession = ({ sessions, onStartSession, onCreateSession, onDelete
{sessions.map((session) => (
<li key={session.id} className="sm:grid sm:grid-cols-7 flex flex-col sm:mt-0 mt-6 items-center text-sm text-gray-800 gap-4">
<span className="col-span-2 truncate whitespace-nowrap font-bold">{session.name}</span>
<span className="col-span-2 text-gray-600 truncate whitespace-nowrap">editado {session.lastEdited}</span>
<span className="col-span-2 text-gray-600 truncate whitespace-nowrap">expira em {session.lifeSpan} dias</span>
<span className="col-span-2 text-gray-600 truncate whitespace-nowrap">editado {toHumanReadableTimeDiff(session.lastEdited)}</span>
<span className="col-span-2 text-gray-600 truncate whitespace-nowrap">expira {toHumanReadableTimeDiff(session.expirationTime)}</span>
<div className="col-span-1 flex justify-end space-x-4">
<a
href="#"
Expand Down
Loading
Loading