session lobby wip

This commit is contained in:
Nuwan 2023-12-14 04:19:25 +05:30
parent 4fed7cbb1a
commit cff277d437
14 changed files with 407 additions and 80 deletions

View File

@ -1,39 +1,55 @@
import React from 'react';
import React, { useEffect } from 'react';
import { Col, Row, Card, CardBody } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import { useResponsive } from '@farfetch/react-context-responsive';
import JKLobbyUserList from '../sessions/JKLobbyUserList';
import JKLobbyChat from '../sessions/JKLobbyChat';
import { useDispatch, useSelector } from 'react-redux';
import { fetchOnlineMusicians } from '../../store/features/onlineMusiciansSlice';
import { fetchUserLatencies } from '../../store/features/latencySlice';
import { useAuth } from '../../context/UserAuth';
function JKMusicSessionsLobby() {
const { t } = useTranslation();
const { greaterThan } = useResponsive();
const dispatch = useDispatch();
const { currentUser } = useAuth();
const onlineMusicians = useSelector(state => state.onlineMusician.musicians);
const loadingStatus = useSelector(state => state.onlineMusician.status);
useEffect(() => {
dispatch(fetchOnlineMusicians());
}, []);
useEffect(() => {
if (loadingStatus === 'succeeded' && onlineMusicians.length > 0) {
const userIds = onlineMusicians.map(p => p.id);
const options = { currentUserId: currentUser.id, participantIds: userIds };
dispatch(fetchUserLatencies(options));
}
}, [loadingStatus]);
return (
<div>
<>
<Card>
<FalconCardHeader title={t('lobby.page_title', { ns: 'sessions' })} titleClass="font-weight-bold" />
<CardBody className="pt-0">
{greaterThan.sm ? (
<Row className="justify-content-between">
<Col>
<div className="table-responsive-xl px-2">
<JKLobbyUserList />
</div>
</Col>
<Col>
<JKLobbyChat />
</Col>
</Row>
) : (
<Row className="swiper-container d-block d-md-none" data-testid="sessionsSwiper">
<Row className="justify-content-between">
<Col>
<div className="table-responsive-xl px-2">
<JKLobbyUserList loadingStatus={loadingStatus} onlineMusicians={onlineMusicians} />
</div>
</Col>
<Col>
<JKLobbyChat />
</Col>
</Row>
) : (
<Row className="swiper-container d-block d-md-none" data-testid="sessionsSwiper" />
)}
</CardBody>
</Card>

View File

@ -0,0 +1,20 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useTranslation } from 'react-i18next';
function JKMoreDetailsButton({ toggleMoreDetails, cssClasses, children}) {
const { t } = useTranslation();
return (
<a href="/#" onClick={toggleMoreDetails} data-testid="btnMore">
<span
className={cssClasses}
data-bs-toggle="tooltip"
title={t('view_profile', { ns: 'people' })}
>
{ children }
</span>
</a>
);
}
export default JKMoreDetailsButton;

View File

@ -1,21 +1,82 @@
import React from 'react';
import { Card, CardBody, CardFooter, CardHeader, CardText, CardTitle, Button } from 'reactstrap';
import React, { useState, useRef, useEffect } from 'react';
import { Container, Row, Col, Button } from 'reactstrap';
import { useDispatch, useSelector } from 'react-redux';
import { fetchLobbyChatMessages } from '../../store/features/lobbyChatMessagesSlice';
import { useAuth } from '../../context/UserAuth';
function JKLobbyChat() {
const CHANNEL_LOBBY = 'lobby';
const [newMessage, setNewMessage] = useState('');
const dispatch = useDispatch();
const messageTextBox = useRef();
const { currentUser } = useAuth();
const chatMessages = useSelector(state => state.lobbyChat.messages);
useEffect(() => {
dispatch(fetchLobbyChatMessages());
}, []);
const handleOnKeyPress = event => {
if (event.key === 'Enter' || event.key === 'NumpadEnter') {
event.preventDefault();
sendMessage();
event.target.value = '';
}
};
const sendMessage = () => {
let msgData = {
message: newMessage,
channel: CHANNEL_LOBBY,
user_id: currentUser.id,
};
setNewMessage('');
try {
//dispatch(postNewMessage(msgData));
} catch (err) {
console.log('addNewMessage error', err);
} finally {
}
};
return (
// <Card
// className="my-2"
// >
// <CardHeader>Header</CardHeader>
// <CardBody>
// <CardTitle tag="h5">Special Title Treatment</CardTitle>
// <CardText>With supporting text below as a natural lead-in to additional content.</CardText>
// <Button>Go somewhere</Button>
// </CardBody>
// </Card>
<div>
<div className='bg-200 text-900' style={{ padding: '0.75rem'}}>Header</div>
<div className='border pt-1 pl-3 p-2'>Body</div>
<div className="bg-200 text-900" style={{ padding: '0.75rem' }}>
Lobby Chat
</div>
<div className="border pt-1 pl-3 p-2">
<Container>
<Row>
<Col>
{chatMessages.map((msg, index) => (
<div key={index}>
<span className="text-primary">{msg.user_id}</span> : {msg.message}
</div>
))}
</Col>
</Row>
<Row>
<Col>
<textarea
style={{ width: '100%' }}
value={newMessage}
onChange={e => setNewMessage(e.target.value)}
onKeyPress={handleOnKeyPress}
ref={messageTextBox}
/>
</Col>
</Row>
<Row>
<Col className="d-flex justify-content-end">
<Button color="primary" onClick={sendMessage} disabled={!newMessage}>
Send Message
</Button>
</Col>
</Row>
</Container>
</div>
</div>
);
}

View File

@ -1,22 +1,71 @@
import React from 'react';
import React, { useState } from 'react';
import { Row } from 'reactstrap';
import JKProfileAvatar from '../profiles/JKProfileAvatar';
import JKProfileAvatar from '../profile/JKProfileAvatar';
import JKConnectButton from '../profile/JKConnectButton';
import JKMessageButton from '../profile/JKMessageButton';
import JKMoreDetailsButton from '../profile/JKMoreDetailsButton';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import JKProfileSidePanel from '../profile/JKProfileSidePanel';
import { useSelector, useDispatch } from 'react-redux';
import { fetchPerson } from '../../store/features/peopleSlice';
import { useAuth } from '../../context/UserAuth';
function JKLobbyUser({ user }) {
const [showSidePanel, setShowSidePanel] = useState(false)
const { currentUser } = useAuth();
const dispatch = useDispatch();
const latencyData = useSelector(state => state.latency.latencies.find(l => l.user_id === user.id));
const userData = useSelector(state => state.people.people.find(p => p.id === user.id));
const toggleMoreDetails = async (e) => {
e.preventDefault();
try {
await dispatch(fetchPerson({ userId: user.id })).unwrap();
} catch (error) {
console.log(error);
}
setShowSidePanel(prev => !prev);
};
function JKLobbyUser() {
return (
<Row className="d-flex flex-row justify-content-between">
<input type="checkbox" className="align-middle" />
<div className="avatar avatar-sm">
<a href="/#" onClick={toggleMoreDetails}>
<JKProfileAvatar src={user.photo_url} />
</a>
</div>
<div className="ml-2 ms-2" style={{ width: '70%' }}>
<a href="/#" onClick={toggleMoreDetails}>
{user.name}
</a>
</div>
</Row>
<>
<tr>
<td className="align-middle">
<Row className="d-flex flex-row justify-content-between" style={{ alignItems: 'center' }}>
<input type="checkbox" className="align-middle" />
<div className="avatar avatar-sm">
<a href="/#" onClick={toggleMoreDetails}>
<JKProfileAvatar src={user.photo_url} />
</a>
</div>
<div className="ml-2 ms-2" style={{ width: '70%' }}>
<a href="/#" onClick={toggleMoreDetails}>
{user.name}
</a>
</div>
</Row>
</td>
<td className="align-middle text-center">
<JKConnectButton
currentUser={currentUser}
user={user}
addContent={<FontAwesomeIcon icon="plus" transform="shrink-4 down-1" className="mr-1" />}
removeContent={<FontAwesomeIcon icon="minus" transform="shrink-4 down-1" className="mr-1" />}
cssClasses="fs--1 px-2 py-1 mr-1"
/>
<JKMessageButton currentUser={currentUser} user={user} cssClasses="fs--1 px-2 py-1 mr-1">
<FontAwesomeIcon icon="comments" transform="shrink-4 down-1" className="mr-1" />
</JKMessageButton>
<JKMoreDetailsButton toggleMoreDetails={toggleMoreDetails} cssClasses="btn btn-primary fs--1 px-2 py-1">
<FontAwesomeIcon icon="user" transform="shrink-4 down-1" className="mr-1" />
</JKMoreDetailsButton>
</td>
</tr>
<JKProfileSidePanel user={userData} latencyData={latencyData} show={showSidePanel} setShow={setShowSidePanel} />
</>
);
}

View File

@ -2,32 +2,40 @@ import React from 'react';
import { Table } from 'reactstrap';
import { useTranslation } from 'react-i18next';
import JKLobbyUser from './JKLobbyUser';
import { isIterableArray } from '../../helpers/utils';
import Loader from '../common/Loader';
function JKLobbyUserList() {
function JKLobbyUserList({ loadingStatus, onlineMusicians}) {
const { t } = useTranslation();
return (
<Table striped bordered className="fs--1" data-testid="sessionsLobbyUsersTable">
<thead className="bg-200 text-900">
<tr>
<th width="35%" scope="col">
{t('lobby.header.musician', { ns: 'sessions' })}
</th>
<th scope="col" className="text-center">
{t('actions', { ns: 'common' })}
</th>
</tr>
</thead>
<tbody className="list">
<tr>
<td className="align-middle">
<JKLobbyUser />
</td>
<td className="align-middle text-center">
Actions
</td>
</tr>
</tbody>
</Table>
<>
{loadingStatus === 'loading' && onlineMusicians.length === 0 ? (
<Loader />
) : (
<Table striped bordered className="fs--1" data-testid="sessionsLobbyUsersTable">
<thead className="bg-200 text-900">
<tr>
<th width="35%" scope="col">
{t('lobby.header.musician', { ns: 'sessions' })}
</th>
<th scope="col" className="text-center">
{t('actions', { ns: 'common' })}
</th>
</tr>
</thead>
<tbody className="list">
{isIterableArray(onlineMusicians) ? (
onlineMusicians.map(musician => <JKLobbyUser key={musician.id} user={musician} />)
) : (
<tr>
<td colSpan={2}>No users currently online.</td>
</tr>
)}
</tbody>
</Table>
)}
</>
);
}

View File

@ -35,6 +35,14 @@ export const getPeople = ({ data, offset, limit } = {}) => {
})
}
export const getPeopleIndex = () => {
return new Promise((resolve, reject) => {
apiFetch(`/users`)
.then(response => resolve(response))
.catch(error => reject(error))
})
}
export const getGenres = () => {
return new Promise((resolve, reject) => {
apiFetch('/genres')
@ -107,6 +115,17 @@ export const createTextMessage = (options) => {
})
}
export const createLobbyChatMessage = (options) => {
return new Promise((resolve, reject) => {
apiFetch(`/chat`, {
method: "POST",
body: JSON.stringify(options)
})
.then(response => resolve(response))
.catch(error => reject(error))
})
}
export const getNotifications = (userId, options = {}) => {
return new Promise((resolve, reject) => {
apiFetch(`/users/${userId}/notifications?${new URLSearchParams(options)}`)
@ -153,3 +172,11 @@ export const getLatencyToUsers = (currentUserId, participantIds) => {
.catch(error => reject(error))
})
}
export const getLobbyChatMessages = (options = {}) => {
return new Promise((resolve, reject) => {
apiFetch(`/chat_messages?${new URLSearchParams(options)}`)
.then(response => resolve(response))
.catch(error => reject(error))
})
}

View File

@ -0,0 +1,48 @@
import { useDispatch, useSelector } from 'react-redux';
import { fetchMessagesByReceiverId, postNewMessage } from '../../store/features/textMessagesSlice';
export const useMessage = () => {
const messages = useSelector(state =>
state.textMessage.messages
.filter(
message =>
(message.senderId === user.id && message.receiverId === window.currentUser.id) ||
(message.senderId === window.currentUser.id && message.receiverId === user.id)
)
.sort((a, b) => {
return new Date(a.createdAt) - new Date(b.createdAt);
})
);
const fetchMessages = async () => {
try {
const options = { userId: user.id, offset: offset, limit: LIMIT };
setFetching(true);
await dispatch(fetchMessagesByReceiverId(options)).unwrap();
if(messages.length < LIMIT){
goToBottom();
}
} catch (err) {
console.log('ERROR', err);
} finally {
setFetching(false);
}
};
const sendMessage = () => {
let msgData = {
message: newMessage,
target_user_id: user.id,
source_user_id: currentUser.id
};
setNewMessage('');
try {
dispatch(postNewMessage(msgData));
} catch (err) {
console.log('ERROR', err);
}
};
return { messages, fetchMessages, sendMessage}
}

View File

@ -34,6 +34,7 @@ export const latencySlice = createSlice({
const unique = [];
records.map(x => unique.filter(a => a.user_id === x.user_id).length > 0 ? null : unique.push(x))
state.latencies = unique
state.status = 'succeeded'
})
.addCase(fetchUserLatencies.rejected, (state, action) => {
console.log("_DEBUG_ fail fetchUserLatencies", action.payload);

View File

@ -0,0 +1,48 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { getLobbyChatMessages, createLobbyChatMessage } from '../../helpers/rest';
export const fetchLobbyChatMessages = createAsyncThunk('chatMessage/fetchLobbyChatMessages', async (options, thunkAPI) => {
const response = await getLobbyChatMessages(options);
return response.json();
});
export const postNewChatMessage = createAsyncThunk('chatMessage/postNewChatMessage', async (options, thunkAPI) => {
const response = await createLobbyChatMessage(options);
return response.json();
});
const chatMessagesSlice = createSlice({
name: 'chatMessages',
initialState: {
messages: [],
status: 'idel',
error: null
},
reducers: {
addMessage(state, action) {
state.messages.push(action.payload);
}
},
extraReducers: builder => {
builder
.addCase(fetchLobbyChatMessages.pending, (state, action) => {
state.status = 'loading';
})
.addCase(fetchLobbyChatMessages.fulfilled, (state, action) => {
const records = [...state.messages, ...action.payload.messages];
state.messages = records;
state.status = 'succeeded';
})
.addCase(fetchLobbyChatMessages.rejected, (state, action) => {
state.error = action.payload;
state.status = 'failed';
})
.addCase(postNewChatMessage.fulfilled, (state, action) => {
state.messages.push(action.payload);
})
}
});
export const { addMessage } = chatMessagesSlice.actions;
export default chatMessagesSlice.reducer;

View File

@ -0,0 +1,45 @@
import {createSlice, createAsyncThunk} from "@reduxjs/toolkit";
import { getPeopleIndex } from "../../helpers/rest";
const initialState = {
musicians: [],
status: 'idle',
error: null,
}
export const fetchOnlineMusicians = createAsyncThunk(
'onlineMusician/fetchMusicians',
async (options, thunkAPI) => {
const response = await getPeopleIndex(options)
return response.json()
}
)
export const onlineMusiciansSlice = createSlice({
name: 'onlineMusicians',
initialState,
reducers: {
add: (state, action) => {
state.musicians.push(action.payload)
},
remove: (state, action) => {
state.musicians = state.musicians.filter(musician => musician.id !== action.payload.id)
}
},
extraReducers: (builder) => {
builder
.addCase(fetchOnlineMusicians.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchOnlineMusicians.fulfilled, (state, action) => {
state.status = 'succeeded'
state.musicians = action.payload
})
.addCase(fetchOnlineMusicians.rejected, (state, action) => {
state.status = 'failed'
state.error = action.payload
})
}
})
export default onlineMusiciansSlice.reducer

View File

@ -67,7 +67,6 @@ export const textMessageSlice = createSlice({
const unique = [];
mergedMsgs.map(x => unique.filter(a => a.id === x.id).length > 0 ? null : unique.push(x));
state.messages = unique
console.log("fetchMessagesByReceiverId.fulfilled");
})
.addCase(fetchMessagesByReceiverId.rejected, (state, action) => {
state.status = 'failed'

View File

@ -1,16 +1,20 @@
import { configureStore } from "@reduxjs/toolkit"
import textMessageReducer from "./features/textMessagesSlice"
import peopleSlice from "./features/peopleSlice"
import sessionSlice from "./features/sessionsSlice"
import notificationSlice from './features/notificationSlice'
import latencySlice from "./features/latencySlice"
import lobbyChatMessagesReducer from "./features/lobbyChatMessagesSlice"
import peopleReducer from "./features/peopleSlice"
import onlineMusicianReducer from "./features/onlineMusiciansSlice"
import sessionReducer from "./features/sessionsSlice"
import notificationReducer from './features/notificationSlice'
import latencyReducer from "./features/latencySlice"
export default configureStore({
reducer: {
textMessage: textMessageReducer,
people: peopleSlice,
notification: notificationSlice,
session: sessionSlice,
latency: latencySlice
people: peopleReducer,
notification: notificationReducer,
session: sessionReducer,
latency: latencyReducer,
onlineMusician: onlineMusicianReducer,
lobbyChat: lobbyChatMessagesReducer
}
})

View File

@ -5,6 +5,7 @@ module JamRuby
CHANNEL_SESSION = 'session'
CHANNEL_LESSON = 'lesson'
CHANNEL_LOBBY = 'lobby'
self.table_name = 'chat_messages'
self.primary_key = 'id'

View File

@ -163,7 +163,7 @@ module JamRuby
def do_filter(user_ids)
rel = User.musicians.where('users.id <> ?', self.user.id)
#user_ids parameter is used to track users returned from neo4j user's latency data service. #if this variable is not null means we are calling this method with neo4j latency data (currently this call only comes from api_search_controller#filter)
#user_ids: users returned from neo4j latency data service. #if this variable is not null, that means we are calling this method with neo4j latency data (currently this call only comes from api_search_controller#filter)
#debugger
unless user_ids.nil?