Skip to content
Snippets Groups Projects
Unverified Commit badf33ad authored by Khemissi Amir's avatar Khemissi Amir Committed by GitHub
Browse files

Join refactoring: First stage of refactoring. (#3788)

* Join: Added & integrated `usePublicRoom` in `RoomJoin`
	+ Minor bug fix to AuthZ on `meetings_controller`.

* Join: First stages of refactroing.
                    + Marked `meetings_controller#join` as deprecated.
                    + Fixed meeting join low level logic.
                    + Started refactoring the UI.
parent 2e25bcb2
No related branches found
No related tags found
No related merge requests found
......@@ -29,6 +29,7 @@ module Api
end
# GET /api/v1/meetings/:friendly_id/join.json
# **DEPRECATED**.
def join
if authorized_as_viewer? || authorized_as_moderator?
if authorized_as_moderator?
......
import consumer from './consumer';
export default function subscribeToRoom(friendlyId, joinUrl) {
consumer.subscriptions.create({
export default function subscribeToRoom(friendlyId, { onReceived }) {
return consumer.subscriptions.create({
channel: 'RoomsChannel',
friendly_id: friendlyId,
}, {
connected() {},
connected() {
console.info(`WS: Connected to room(friendly_id): ${friendlyId} channel.`);
},
disconnected() {},
disconnected() {
console.info(`WS: Disconnected from room(friendly_id): ${friendlyId} channel.`);
},
received() {
// Called when there's incoming data on the websocket for this channel
window.location.replace(joinUrl);
console.info(`WS: Received join signal on room(friendly_id): ${friendlyId}.`);
if (onReceived) {
onReceived();
}
},
});
}
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import Card from 'react-bootstrap/Card';
import { useParams } from 'react-router-dom';
import {
Button, Col, Row, Stack,
} from 'react-bootstrap';
import toast from 'react-hot-toast';
import FormLogo from '../../shared_components/forms/FormLogo';
import useRoom from '../../../hooks/queries/rooms/useRoom';
import useRoomJoin from '../../../hooks/queries/rooms/useRoomJoin';
import usePublicRoom from '../../../hooks/queries/rooms/usePublicRoom';
import Spinner from '../../shared_components/utilities/Spinner';
import useRoomStatus from '../../../hooks/queries/rooms/useRoomStatus';
import Avatar from '../../users/user/Avatar';
function waitOrJoin(refetchJoin, refetchStatus) {
refetchStatus();
refetchJoin();
}
import subscribeToRoom from '../../../channels/rooms_channel';
export default function RoomJoin() {
const { friendlyId } = useParams();
const [name, setName] = useState('');
const [accessCode, setAccessCode] = useState('');
const [hasStarted, setHasStarted] = useState(false);
const { isLoading, data: room } = useRoom(friendlyId, true);
const { isSuccess: isSuccessJoin, isError: isErrorJoin, refetch: refetchJoin } = useRoomJoin(friendlyId, name, accessCode);
const { isSuccess: isSuccessStatus, isError: isErrorStatus, refetch: refetchStatus } = useRoomStatus(friendlyId, name, accessCode);
const publicRoom = usePublicRoom(friendlyId);
const roomStatus = useRoomStatus(friendlyId, name, accessCode);
if (isLoading) return <Spinner />;
// eslint-disable-next-line consistent-return
useEffect(() => {
// Room channel subscription:
if (roomStatus.isSuccess && roomStatus.isFetchedAfterMount) {
// When the user provides valid input (name, codes) the UI will subscribe to the room channel.
const channel = subscribeToRoom(friendlyId, { onReceived: () => { setHasStarted(true); } });
// Cleanup: On component unmouting any opened channel subscriptions will be closed.
return () => {
channel.unsubscribe();
console.info(`WS: unsubscribed from room(friendly_id): ${friendlyId} channel.`);
};
}
}, [roomStatus.isFetchedAfterMount, roomStatus.isSuccess]);
useEffect(() => {
// Meeting started:
// When meeting starts thig logic will be fired, indicating the event to waiting users (thorugh a toast) for UX matter.
// Logging the event for debugging purposes and refetching the join logic with the user's given input (name & codes).
// With a delay of 7s to give reasonable time for the meeting to fully start on the BBB server.
if (hasStarted) {
toast.success('Meeting started.');
console.info(`Attempting to join the room(friendly_id): ${friendlyId} meeting in 7s.`);
setTimeout(roomStatus.refetch, 7000); // TODO: Improve this race condition handling by the backend.
}
}, [hasStarted]);
useEffect(() => {
// UI synchronization on failing join attempt:
// When the room status API returns an error indicating a failed join attempt it's highly due to stale credentials.
// In such case, users from a UX perspective will appreciate having the UI updated informing them about the case.
// i.e: Indicating the lack of providing access code value for cases where access code was generated while the user was waiting.
if (roomStatus.isError) {
publicRoom.refetch();
}
}, [roomStatus.isError]);
if (publicRoom.isLoading) return <Spinner />;
return (
<div className="vertical-center">
......@@ -34,24 +66,17 @@ export default function RoomJoin() {
<Card className="col-md-6 mx-auto p-0 border-0 shadow-sm">
<Card.Body className="p-4">
<Row>
<Col>
<Stack>
<p className="text-muted mb-0">You have been invited to join</p>
<h1>
{ room.name }
{publicRoom.data.name}
{publicRoom.isFetching && <Spinner />}
</h1>
</Stack>
</Col>
<Col className="col-4">
<Stack>
<Avatar className="d-block m-auto" avatar={room.owner_avatar} radius={100} />
<span className="float-end text-center mt-2">{room.owner_name}</span>
</Stack>
</Col>
</Row>
</Card.Body>
<Card.Footer className="p-4 bg-white">
{ (isSuccessStatus || isSuccessJoin) ? (
{(roomStatus.isSuccess && !roomStatus.data.status) ? (
<div className="mt-3">
<Row>
<Col className="col-10">
......@@ -71,10 +96,11 @@ export default function RoomJoin() {
id="join-name"
placeholder="Enter your name"
className="form-control"
value={name}
onChange={(event) => setName(event.target.value)}
/>
</label>
{ room?.viewer_access_code
{publicRoom.data?.viewer_access_code
&& (
<div className="mt-2">
<label htmlFor="access-code" className="small text-muted d-block"> Access Code
......@@ -83,18 +109,19 @@ export default function RoomJoin() {
id="access-code"
placeholder="Enter the access code"
className="form-control"
value={accessCode}
onChange={(event) => setAccessCode(event.target.value)}
/>
</label>
{
(isErrorJoin || isErrorStatus)
(roomStatus.isError)
&& (
<p className="text-danger"> Wrong access code. </p>
)
}
</div>
)}
{ (!(room?.viewer_access_code) && room?.moderator_access_code)
{(!(publicRoom.data?.viewer_access_code) && publicRoom.data?.moderator_access_code)
&& (
<div className="mt-2">
<label htmlFor="access-code" className="small text-muted d-block"> Moderator Access Code (optional)
......@@ -103,18 +130,26 @@ export default function RoomJoin() {
id="access-code"
placeholder="Enter the access code"
className="form-control"
value={accessCode}
onChange={(event) => setAccessCode(event.target.value)}
/>
</label>
{
(isErrorJoin || isErrorStatus)
(roomStatus.isError)
&& (
<p className="text-danger"> Wrong access code. </p>
)
}
</div>
)}
<Button className="mt-3 d-block float-end" onClick={() => waitOrJoin(refetchJoin, refetchStatus)}>Join Session</Button>
<Button
className="mt-3 d-block float-end"
onClick={roomStatus.refetch}
disabled={publicRoom.isFetching || roomStatus.isFetching}
>
Join Session
{roomStatus.isFetching && <Spinner />}
</Button>
</>
)}
</Card.Footer>
......
import { useQuery } from 'react-query';
import axios from '../../../helpers/Axios';
export default function usePublicRoom(friendlyId) {
return useQuery(
'getRoom',
() => axios.get(`/rooms/${friendlyId}/public.json`).then((resp) => resp.data.data),
{
cacheTime: 0, // No caching.
},
);
}
import { useQuery } from 'react-query';
import axios from '../../../helpers/Axios';
import subscribeToRoom from '../../../channels/rooms_channel';
export default function useRoomJoin(friendlyId, name, accessCode) {
const params = {
name,
access_code: accessCode,
};
return useQuery(
['getRoomJoin', name],
() => axios.get(`/meetings/${friendlyId}/join.json`, { params }).then((resp) => { subscribeToRoom(friendlyId, resp.data.data); }),
{ enabled: false, retry: false },
);
}
import toast from 'react-hot-toast';
import { useQuery } from 'react-query';
import axios from '../../../helpers/Axios';
......@@ -8,12 +9,20 @@ export default function useRoomStatus(friendlyId, name, accessCode) {
};
return useQuery(
['getRoomStatus', name],
() => axios.get(`/meetings/${friendlyId}/status.json`, { params }).then((resp) => {
const response = resp.data.data;
if (response.status) {
window.location.replace(response.joinUrl);
() => axios.get(`/meetings/${friendlyId}/status.json`, { params }).then((resp) => resp.data.data),
{
onSuccess: ({ joinUrl }) => {
if (joinUrl) {
toast.loading('Joining the meeting...');
window.location.replace(joinUrl);
}
}),
{ enabled: false, retry: false },
},
onError: () => {
toast.error('There was a problem completing that action. \n Please try again.');
},
enabled: false,
retry: false,
cacheTime: 0, // No caching.
},
);
}
......@@ -20,11 +20,13 @@ RSpec.describe Api::V1::MeetingsController, type: :controller do
.and_return(meeting_starter_response)
end
it 'makes a call to the MeetingStarter service with the right values' do
it 'makes a call to the MeetingStarter service with the right values and returns the join url' do
logout = 'http://example.com'
request.env['HTTP_REFERER'] = logout
presentation_url = nil
allow_any_instance_of(BigBlueButtonApi).to receive(:join_meeting).and_return('JOIN_URL')
expect(
MeetingStarter
).to receive(:new).with(
......@@ -35,9 +37,15 @@ RSpec.describe Api::V1::MeetingsController, type: :controller do
recording_ready: recording_ready_url,
current_user: user,
provider: 'greenlight'
)
).and_call_original
expect_any_instance_of(MeetingStarter).to receive(:call)
expect_any_instance_of(BigBlueButtonApi).to receive(:join_meeting).with(room:, name: user.name, role: 'Moderator')
post :start, params: { friendly_id: room.friendly_id }
expect(response).to have_http_status(:created)
expect(JSON.parse(response.body)['data']).to eq('JOIN_URL')
end
it 'cannot make call to MeetingStarter service for another room' do
......@@ -109,6 +117,7 @@ RSpec.describe Api::V1::MeetingsController, type: :controller do
end
end
# **DEPRECATED**.
describe '#join' do
before do
allow_any_instance_of(Room).to receive(:viewer_access_code).and_return('')
......@@ -202,9 +211,24 @@ RSpec.describe Api::V1::MeetingsController, type: :controller do
room = create(:room, user:)
allow_any_instance_of(BigBlueButtonApi).to receive(:meeting_running?).and_return(true)
allow_any_instance_of(BigBlueButtonApi).to receive(:join_meeting).and_return('JOIN_URL')
expect_any_instance_of(BigBlueButtonApi).to receive(:join_meeting).with(room:, name: user.name, role: 'Viewer')
get :status, params: { friendly_id: room.friendly_id, name: user.name }
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)['data']).to eq({ 'joinUrl' => 'JOIN_URL', 'status' => true })
end
it 'returns status false if the meeting is NOT running' do
room = create(:room, user:)
allow_any_instance_of(BigBlueButtonApi).to receive(:meeting_running?).and_return(false)
expect_any_instance_of(BigBlueButtonApi).not_to receive(:join_meeting)
get :status, params: { friendly_id: room.friendly_id, name: user.name }
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)['data']).to eq({ 'status' => false })
end
it 'joins as viewer if no access code is required nor provided' do
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment