wip session lobby

This commit is contained in:
Nuwan 2023-12-20 21:32:01 +05:30
parent cff277d437
commit 0f25b00571
10 changed files with 483 additions and 45 deletions

View File

@ -89,6 +89,7 @@ function JKDashboardMain() {
registerTextMessageCallback(); registerTextMessageCallback();
registerFriendRequest(); registerFriendRequest();
registerFriendRequestAccepted(); registerFriendRequestAccepted();
registerChatMessageCallback();
scriptLoaded.current = true scriptLoaded.current = true
}; };
@ -125,10 +126,19 @@ function JKDashboardMain() {
}); });
}; };
const registerChatMessageCallback = () => {
window.JK.JamServer.registerMessageCallback(window.JK.MessageType.CHAT_MESSAGE, function (header, payload) {
console.log("registerChatMessageCallback " + JSON.stringify(payload));
// chatMessageReceived(payload);
// context.ChatActions.msgReceived(payload);
// handledNotification(payload);
});
}
const registerFriendRequest = () => { const registerFriendRequest = () => {
window.JK.JamServer.registerMessageCallback(window.JK.MessageType.FRIEND_REQUEST, function(header, payload) { window.JK.JamServer.registerMessageCallback(window.JK.MessageType.FRIEND_REQUEST, function(header, payload) {
console.log('registerFriendRequest payload', payload);
console.log('registerFriendRequest header', header);
handleNotification(payload, header.type); handleNotification(payload, header.type);
}); });
}; };

View File

@ -0,0 +1,191 @@
import React, { useState, useRef, useEffect } from 'react';
import { Container, Row, Col, Button } from 'reactstrap';
import { useDispatch, useSelector } from 'react-redux';
import { fetchLobbyChatMessages, postNewChatMessage } from '../../store/features/lobbyChatMessagesSlice';
import { useAuth } from '../../context/UserAuth';
import JKProfileAvatar from '../profile/JKProfileAvatar';
import TimeAgo from '../common/JKTimeAgo';
import { Scrollbar } from 'react-scrollbars-custom';
function JKLobbyChat() {
const CHANNEL_LOBBY = 'lobby';
const LIMIT = 10;
const [newMessage, setNewMessage] = useState('');
const dispatch = useDispatch();
const messageTextBox = useRef();
const scrollbar = useRef();
const scrolledToBottom = useRef(false);
const { currentUser } = useAuth();
const [fetching, setFetching] = useState(false);
const [messagesArrived, setMessagesArrived] = useState(false);
const [offset, setOffset] = useState(0);
const chatMessages = useSelector(state => state.lobbyChat.messages);
const createStatus = useSelector(state => state.lobbyChat.create_status);
const fetchMessages = async () => {
const options = { offset: offset, limit: LIMIT };
try {
setFetching(true);
await dispatch(fetchLobbyChatMessages(options)).unwrap();
if(chatMessages.length < LIMIT){
goToBottom();
}
} catch (err) {
console.log('ERROR', err);
} finally {
setFetching(false);
}
};
useEffect(() => {
fetchMessages();
}, []);
useEffect(() => {
if (scrollbar && scrollbar.current) {
if (!fetching && !scrollAtTop()) {
if (chatMessages[chatMessages.length - 1]['user_id'] !== currentUser.id) {
if (!scrolledToBottom.current) {
setMessagesArrived(true);
} else {
goToBottom();
}
} else {
goToBottom();
}
}
}
}, [chatMessages]);
useEffect(() => {
if (!messagesArrived) {
goToBottom();
}
}, [messagesArrived]);
useEffect(() => {
if (offset !== 0) {
fetchMessages();
}
}, [offset]);
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(postNewChatMessage(msgData));
} catch (err) {
console.log('addNewMessage error', err);
}
};
useEffect(() => {
if (createStatus === 'succeeded') {
fetchMessages();
messageTextBox.current.focus();
}
}, [createStatus]);
const scrollAtTop = () => {
return scrollbar.current.scrollTop === 0;
};
const goToBottom = () => {
if (scrollbar && scrollbar.current) {
scrollbar.current.scrollToBottom();
}
};
const handleScrollStop = scrollValues => {
console.log('handleScrollStop', scrollValues);
scrolledToBottom.current = false;
if (scrollValues.scrollTop === 0) {
setOffset(prev => prev + 1);
} else if (scrollValues.scrollTop === scrollValues.scrollHeight - scrollValues.clientHeight) {
scrolledToBottom.current = true;
setMessagesArrived(false);
}
};
const containerStyle = {
display: 'flex',
flexDirection: 'column',
height: '200'
};
return (
<div>
<div className="bg-200 text-900" style={{ padding: '0.75rem' }}>
Lobby Chat
</div>
<div className="border pt-1 pl-3 p-2" style={containerStyle}>
<div style={{ height: '400px' }}>
<Scrollbar
ref={scrollbar}
onScrollStop={handleScrollStop}
style={{ width: '100%', height: 400 }}
mobileNative={true}
trackClickBehavior="step"
>
{chatMessages.map(message => (
<div className="d-flex mb-3 mr-1 text-message-row" key={message.id}>
<div className="avatar avatar-2xl d-inline-block">
<JKProfileAvatar url={message.user.photo_url} />
</div>
<div className="d-inline-block">
<div className="d-flex flex-column">
<div>
<strong>{message.user.name}</strong>
<time className="notification-time ml-2 t-1">
<TimeAgo date={message.created_at} />
</time>
</div>
<div>{message.message}</div>
</div>
</div>
</div>
))}
</Scrollbar>
{messagesArrived && (
<div className="d-flex justify-content-center">
<Button color="info" size="sm" onClick={() => setMessagesArrived(prev => !prev)}>
New messages
</Button>
</div>
)}
</div>
<div className="mt-2" style={{ height: '20%' }}>
<textarea
style={{ width: '100%' }}
value={newMessage}
onChange={e => setNewMessage(e.target.value)}
onKeyPress={handleOnKeyPress}
ref={messageTextBox}
/>
</div>
<div className="d-flex justify-content-end" style={{ height: '10%' }}>
<Button color="primary" onClick={sendMessage} disabled={!newMessage}>
Send Message
</Button>
</div>
</div>
</div>
);
}
export default JKLobbyChat;

View File

@ -1,22 +1,97 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { Container, Row, Col, Button } from 'reactstrap'; import { Container, Row, Col, Button } from 'reactstrap';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { fetchLobbyChatMessages } from '../../store/features/lobbyChatMessagesSlice'; import { fetchLobbyChatMessages, postNewChatMessage } from '../../store/features/lobbyChatMessagesSlice';
import { useAuth } from '../../context/UserAuth'; import { useAuth } from '../../context/UserAuth';
import JKProfileAvatar from '../profile/JKProfileAvatar';
import TimeAgo from '../common/JKTimeAgo';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import useOnScreen from '../../hooks/useOnScreen';
import useKeepScrollPosition from '../../hooks/useKeepScrollPosition';
function JKLobbyChat() { function JKLobbyChat() {
const CHANNEL_LOBBY = 'lobby'; const CHANNEL_LOBBY = 'lobby';
const LIMIT = 10;
const [newMessage, setNewMessage] = useState(''); const [newMessage, setNewMessage] = useState('');
const dispatch = useDispatch(); const dispatch = useDispatch();
const messageTextBox = useRef(); const messageTextBox = useRef();
const scrollbar = useRef();
const scrolledToBottom = useRef(false);
const { currentUser } = useAuth(); const { currentUser } = useAuth();
const [fetching, setFetching] = useState(false);
const [messagesArrived, setMessagesArrived] = useState(false);
const [offset, setOffset] = useState(0);
const chatMessages = useSelector(state => state.lobbyChat.messages); const chatMessages = useSelector(state => state.lobbyChat.records.messages);
const next = useSelector(state => state.lobbyChat.records.next);
const createStatus = useSelector(state => state.lobbyChat.create_status);
const [messages, setMessages] = useState([]);
const [lastMessageRef, setLastMessageRef] = useState(null);
const isIntersecting = useOnScreen({ current: lastMessageRef });
const { containerRef } = useKeepScrollPosition([messages]);
const fetchMessages = async (overrides = {}) => {
const options = { start: offset * LIMIT, limit: LIMIT };
const params = { ...options, ...overrides };
try {
setFetching(true);
await dispatch(fetchLobbyChatMessages(params)).unwrap();
} catch (error) {
console.log('Error when fetching chat messages', error);
} finally {
setFetching(false);
}
};
useEffect(() => { useEffect(() => {
dispatch(fetchLobbyChatMessages()); if (isIntersecting) {
if (next) {
setOffset(prev => prev + 1);
}
}
}, [isIntersecting]);
useEffect(() => {
if (offset !== 0) {
fetchMessages();
}
}, [offset]);
useEffect(() => {
fetchMessages();
}, []); }, []);
useEffect(() => {
setMessages(old => {
const chats = old.concat(chatMessages);
const deliveredChats = chats.filter((chat, index) => {
return chat.status !== 'pending';
});
return deliveredChats.sort((a, b) => {
return new Date(a.created_at) - new Date(b.created_at);
});
});
if (!scrollBarAtBottom(containerRef.current)) {
if (messages.length > 0 && messages[messages.length - 1]['user_id'] !== currentUser.id) {
setMessagesArrived(true);
}
}
}, [chatMessages]);
const scrollBarAtBottom = el => {
let sh = el.scrollHeight,
st = el.scrollTop,
ht = el.offsetHeight;
return ht == 0 || st == sh - ht;
};
const goToBottom = () => {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
};
const handleOnKeyPress = event => { const handleOnKeyPress = event => {
if (event.key === 'Enter' || event.key === 'NumpadEnter') { if (event.key === 'Enter' || event.key === 'NumpadEnter') {
event.preventDefault(); event.preventDefault();
@ -27,55 +102,103 @@ function JKLobbyChat() {
const sendMessage = () => { const sendMessage = () => {
let msgData = { let msgData = {
id: Date.now(),
message: newMessage, message: newMessage,
channel: CHANNEL_LOBBY, channel: CHANNEL_LOBBY,
user_id: currentUser.id, user_id: currentUser.id,
created_at: new Date(),
user: {
id: currentUser.id,
name: currentUser.name,
photo_url: currentUser.photo_url
},
status: 'pending'
}; };
setNewMessage(''); setNewMessage('');
setMessages(old => old.concat([msgData]));
goToBottom();
try { try {
//dispatch(postNewMessage(msgData)); dispatch(postNewChatMessage(msgData));
} catch (err) { } catch (err) {
console.log('addNewMessage error', err); console.log('Error when posting new chat message', err);
} finally {
} }
}; };
useEffect(() => {
if (createStatus === 'succeeded') {
fetchMessages({ start: 0, limit: 1, lastOnly: true });
messageTextBox.current.focus();
}
}, [createStatus]);
const wrapperStyle = {
display: 'flex',
flexDirection: 'column',
height: '200'
};
const containerStyle = {
display: 'flex',
flexDirection: 'column',
height: '400px',
overflow: 'auto'
};
return ( return (
<div> <div>
<div className="bg-200 text-900" style={{ padding: '0.75rem' }}> <div className="bg-200 text-900" style={{ padding: '0.75rem' }}>
Lobby Chat Lobby Chat
</div> </div>
<div className="border pt-1 pl-3 p-2"> <div className="border pt-1 pl-3 p-2" style={wrapperStyle}>
<Container> <div className="lobby-chat" ref={containerRef} style={containerStyle}>
<Row> {messages.map((message, i) => (
<Col> <div className="d-flex mb-3 mr-1 text-message-row" key={message.id}>
{chatMessages.map((msg, index) => ( <div ref={ref => (i === 0 ? setLastMessageRef(ref) : null)}>
<div key={index}> <div className="avatar avatar-2xl d-inline-block">
<span className="text-primary">{msg.user_id}</span> : {msg.message} <JKProfileAvatar url={message.user.photo_url} />
</div> </div>
))} <div className="d-inline-block">
</Col> <div className="d-flex flex-column">
</Row> <div>
<Row> <strong>{message.user.name}</strong>
<Col> <time className="notification-time ml-2 t-1">
<textarea <TimeAgo date={message.created_at} />
style={{ width: '100%' }} </time>
value={newMessage} {message.status === 'pending' && (
onChange={e => setNewMessage(e.target.value)} <span className="ml-2">
onKeyPress={handleOnKeyPress} <FontAwesomeIcon icon="spinner" />
ref={messageTextBox} </span>
/> )}
</Col> </div>
</Row> <div>{message.message}</div>
<Row> </div>
<Col className="d-flex justify-content-end"> </div>
<Button color="primary" onClick={sendMessage} disabled={!newMessage}> </div>
Send Message </div>
))}
{messagesArrived && (
<div className="d-flex justify-content-center">
<Button color="info" size="sm" onClick={() => setMessagesArrived(prev => !prev)}>
New messages
</Button> </Button>
</Col> </div>
</Row> )}
</Container> </div>
<div className="mt-2" style={{ height: '20%' }}>
<textarea
style={{ width: '100%' }}
value={newMessage}
onChange={e => setNewMessage(e.target.value)}
onKeyPress={handleOnKeyPress}
ref={messageTextBox}
/>
</div>
<div className="d-flex justify-content-end" style={{ height: '10%' }}>
<Button color="primary" onClick={sendMessage} disabled={!newMessage}>
Send Message
</Button>
</div>
</div> </div>
</div> </div>
); );

View File

@ -157,7 +157,8 @@ import {
faMusic, faMusic,
faRecordVinyl, faRecordVinyl,
faAddressCard, faAddressCard,
faVolumeUp faVolumeUp,
faSpinner
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
//import { faAcousticGuitar } from "../icons"; //import { faAcousticGuitar } from "../icons";
@ -284,6 +285,7 @@ library.add(
faRecordVinyl, faRecordVinyl,
faAddressCard, faAddressCard,
faVolumeUp, faVolumeUp,
faSpinner,
// Brand // Brand
faFacebook, faFacebook,

View File

@ -175,7 +175,8 @@ export const getLatencyToUsers = (currentUserId, participantIds) => {
export const getLobbyChatMessages = (options = {}) => { export const getLobbyChatMessages = (options = {}) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
apiFetch(`/chat_messages?${new URLSearchParams(options)}`) console.log('getLobbyChatMessages', options)
apiFetch(`/chat?${new URLSearchParams(options)}`)
.then(response => resolve(response)) .then(response => resolve(response))
.catch(error => reject(error)) .catch(error => reject(error))
}) })

View File

@ -0,0 +1,28 @@
import { useRef, useLayoutEffect, useMemo } from "react";
const useKeepScrollPosition = (deps = []) => {
const containerRef = useRef(null);
const previousScrollPosition = useRef(0);
useMemo(() => {
if (containerRef?.current) {
const container = containerRef?.current;
previousScrollPosition.current =
container?.scrollHeight - container?.scrollTop;
}
}, [...deps]);
useLayoutEffect(() => {
if (containerRef?.current) {
const container = containerRef?.current || {};
container.scrollTop =
container?.scrollHeight - previousScrollPosition.current;
}
}, [...deps]);
return {
containerRef
};
};
export default useKeepScrollPosition;

View File

@ -0,0 +1,36 @@
import { useState, useEffect } from "react";
import useOnScreen from "./useOnScreen";
const getMessages = () => {
const data = [];
for (let x = 0; x < 20; x++) {
data.push({
id: faker.datatype.uuid(),
message: faker.lorem.words(Math.floor(Math.random() * 10) + 1),
in: faker.datatype.boolean()
});
}
return data;
};
const useMessages = ({ messageFunc }) => {
const [messages, setMessages] = useState(messageFunc());
const [lastMessageRef, setLastMessageRef] = useState(null);
const isIntersecting = useOnScreen({ current: lastMessageRef });
useEffect(() => {
if (isIntersecting) {
setMessages((old) => messageFunc().concat(old));
}
}, [isIntersecting]);
return {
messages,
setMessages,
setLastMessageRef
};
};
export default useMessages;

View File

@ -0,0 +1,23 @@
import { useState, useEffect } from "react";
const useOnScreen = (ref) => {
const [isIntersecting, setIntersecting] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) =>
setIntersecting(entry.isIntersecting)
);
if (ref.current) {
observer.observe(ref.current);
}
return () => {
observer.disconnect();
};
}, [ref]);
return isIntersecting;
};
export default useOnScreen;

View File

@ -2,7 +2,8 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { getLobbyChatMessages, createLobbyChatMessage } from '../../helpers/rest'; import { getLobbyChatMessages, createLobbyChatMessage } from '../../helpers/rest';
export const fetchLobbyChatMessages = createAsyncThunk('chatMessage/fetchLobbyChatMessages', async (options, thunkAPI) => { export const fetchLobbyChatMessages = createAsyncThunk('chatMessage/fetchLobbyChatMessages', async (options, thunkAPI) => {
const response = await getLobbyChatMessages(options); const params = { ...options, type: 'CHAT_MESSAGE', channel: 'lobby'}
const response = await getLobbyChatMessages(params);
return response.json(); return response.json();
}); });
@ -14,9 +15,13 @@ export const postNewChatMessage = createAsyncThunk('chatMessage/postNewChatMessa
const chatMessagesSlice = createSlice({ const chatMessagesSlice = createSlice({
name: 'chatMessages', name: 'chatMessages',
initialState: { initialState: {
messages: [], records: {
messages: [],
next: null
},
status: 'idel', status: 'idel',
error: null error: null,
create_status: 'idel',
}, },
reducers: { reducers: {
addMessage(state, action) { addMessage(state, action) {
@ -29,16 +34,33 @@ const chatMessagesSlice = createSlice({
state.status = 'loading'; state.status = 'loading';
}) })
.addCase(fetchLobbyChatMessages.fulfilled, (state, action) => { .addCase(fetchLobbyChatMessages.fulfilled, (state, action) => {
const records = [...state.messages, ...action.payload.messages]; console.log('_DEBUG_1 fetchLobbyChatMessages', action.payload);
state.messages = records; //let chats = [...state.records.messages, ...action.payload.chats];
const lastOnly = action.meta.arg.lastOnly;
console.log('_DEBUG_2 fetchLobbyChatMessages', lastOnly);
state.records = {
next: state.records.next === null && lastOnly? null : action.payload.next,
messages: action.payload.chats.map(m => ({...m, status: 'delivered'})).sort((a, b) => {
return new Date(a.created_at) - new Date(b.created_at);
})
};
// state.offset_messages = action.payload.chats.sort((a, b) => {
// return new Date(a.created_at) - new Date(b.created_at);
// });
state.status = 'succeeded'; state.status = 'succeeded';
}) })
.addCase(fetchLobbyChatMessages.rejected, (state, action) => { .addCase(fetchLobbyChatMessages.rejected, (state, action) => {
state.error = action.payload; state.error = action.payload;
state.status = 'failed'; state.status = 'failed';
}) })
.addCase(postNewChatMessage.pending, (state, action) => {
state.create_status = 'loading';
})
.addCase(postNewChatMessage.fulfilled, (state, action) => { .addCase(postNewChatMessage.fulfilled, (state, action) => {
state.messages.push(action.payload); state.create_status = 'succeeded';
})
.addCase(postNewChatMessage.rejected, (state, action) => {
state.create_status = 'failed';
}) })
} }
}); });

View File

@ -93,6 +93,8 @@ module JamRuby
end end
end end
chat_msg.valid?
Rails.logger.info "Chat Message: #{chat_msg.inspect} #{chat_msg.errors.inspect}"
if chat_msg.save if chat_msg.save
ChatMessage.send_chat_msg music_session, chat_msg, source_user, client_id, channel, lesson_session, purpose, target_user, music_notation, recording ChatMessage.send_chat_msg music_session, chat_msg, source_user, client_id, channel, lesson_session, purpose, target_user, music_notation, recording
else else