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();
registerFriendRequest();
registerFriendRequestAccepted();
registerChatMessageCallback();
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 = () => {
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);
});
};

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 { Container, Row, Col, Button } from 'reactstrap';
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 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() {
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 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(() => {
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 => {
if (event.key === 'Enter' || event.key === 'NumpadEnter') {
event.preventDefault();
@ -27,55 +102,103 @@ function JKLobbyChat() {
const sendMessage = () => {
let msgData = {
id: Date.now(),
message: newMessage,
channel: CHANNEL_LOBBY,
user_id: currentUser.id,
created_at: new Date(),
user: {
id: currentUser.id,
name: currentUser.name,
photo_url: currentUser.photo_url
},
status: 'pending'
};
setNewMessage('');
setMessages(old => old.concat([msgData]));
goToBottom();
try {
//dispatch(postNewMessage(msgData));
dispatch(postNewChatMessage(msgData));
} catch (err) {
console.log('addNewMessage error', err);
} finally {
console.log('Error when posting new chat message', err);
}
};
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 (
<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 className="border pt-1 pl-3 p-2" style={wrapperStyle}>
<div className="lobby-chat" ref={containerRef} style={containerStyle}>
{messages.map((message, i) => (
<div className="d-flex mb-3 mr-1 text-message-row" key={message.id}>
<div ref={ref => (i === 0 ? setLastMessageRef(ref) : null)}>
<div className="avatar avatar-2xl d-inline-block">
<JKProfileAvatar url={message.user.photo_url} />
</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
<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>
{message.status === 'pending' && (
<span className="ml-2">
<FontAwesomeIcon icon="spinner" />
</span>
)}
</div>
<div>{message.message}</div>
</div>
</div>
</div>
</div>
))}
{messagesArrived && (
<div className="d-flex justify-content-center">
<Button color="info" size="sm" onClick={() => setMessagesArrived(prev => !prev)}>
New messages
</Button>
</Col>
</Row>
</Container>
</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>
);

View File

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

View File

@ -175,7 +175,8 @@ export const getLatencyToUsers = (currentUserId, participantIds) => {
export const getLobbyChatMessages = (options = {}) => {
return new Promise((resolve, reject) => {
apiFetch(`/chat_messages?${new URLSearchParams(options)}`)
console.log('getLobbyChatMessages', options)
apiFetch(`/chat?${new URLSearchParams(options)}`)
.then(response => resolve(response))
.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';
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();
});
@ -14,9 +15,13 @@ export const postNewChatMessage = createAsyncThunk('chatMessage/postNewChatMessa
const chatMessagesSlice = createSlice({
name: 'chatMessages',
initialState: {
messages: [],
records: {
messages: [],
next: null
},
status: 'idel',
error: null
error: null,
create_status: 'idel',
},
reducers: {
addMessage(state, action) {
@ -29,16 +34,33 @@ const chatMessagesSlice = createSlice({
state.status = 'loading';
})
.addCase(fetchLobbyChatMessages.fulfilled, (state, action) => {
const records = [...state.messages, ...action.payload.messages];
state.messages = records;
console.log('_DEBUG_1 fetchLobbyChatMessages', action.payload);
//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';
})
.addCase(fetchLobbyChatMessages.rejected, (state, action) => {
state.error = action.payload;
state.status = 'failed';
})
.addCase(postNewChatMessage.pending, (state, action) => {
state.create_status = 'loading';
})
.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
chat_msg.valid?
Rails.logger.info "Chat Message: #{chat_msg.inspect} #{chat_msg.errors.inspect}"
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
else