more updates to lobby page

includes showing chat notifications. also ui improvements
This commit is contained in:
Nuwan 2024-01-22 11:28:16 +05:30
parent 5a8c85e765
commit d2c525f498
23 changed files with 401 additions and 180 deletions

View File

@ -12,11 +12,13 @@ import AppContext from '../../context/Context';
import { getPageName } from '../../helpers/utils'; import { getPageName } from '../../helpers/utils';
import useScript from '../../hooks/useScript'; import useScript from '../../hooks/useScript';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { addMessage } from '../../store/features/textMessagesSlice'; import { addMessage } from '../../store/features/textMessagesSlice';
import { add as addNotification } from '../../store/features/notificationSlice'; import { add as addNotification } from '../../store/features/notificationSlice';
import { useLobbyChat } from '../sessions/JKLobbyChatContext';
import { useAuth } from '../../context/UserAuth'; import { useAuth } from '../../context/UserAuth';
import { truncate } from '../../helpers/utils';
import HomePage from '../page/JKHomePage'; import HomePage from '../page/JKHomePage';
@ -34,13 +36,13 @@ import JKMusicSessionsLobby from '../page/JKMusicSessionsLobby';
//import loadable from '@loadable/component'; //import loadable from '@loadable/component';
//const DashboardRoutes = loadable(() => import('../../layouts/JKDashboardRoutes')); //const DashboardRoutes = loadable(() => import('../../layouts/JKDashboardRoutes'));
//const PublicRoutes = loadable(() => import('../../layouts/JKPublicRoutes')) //const PublicRoutes = loadable(() => import('../../layouts/JKPublicRoutes'))
const Msg = ({ closeToast, toastProps, title }) => ( const Msg = ({ closeToast, toastProps, title }) => (
<div> <div>
<a href='#'>{title}</a> <a href="#">{title}</a>
</div> </div>
) );
function JKDashboardMain() { function JKDashboardMain() {
const { isFluid, isVertical, navbarStyle } = useContext(AppContext); const { isFluid, isVertical, navbarStyle } = useContext(AppContext);
@ -48,30 +50,45 @@ function JKDashboardMain() {
const { isAuthenticated, currentUser, setCurrentUser, logout } = useAuth(); const { isAuthenticated, currentUser, setCurrentUser, logout } = useAuth();
const scriptLoaded = useRef(false) const scriptLoaded = useRef(false);
const [showMessageModal, setShowMessageModal] = useState(false) const [showMessageModal, setShowMessageModal] = useState(false);
const [messageUser, setMessageUser] = useState(null) const [messageUser, setMessageUser] = useState(null);
const {
setMessages: setChatMessages,
lobbyChatOffset,
setLobbyChatOffset,
fetchLobbyMessages,
lobbyChatLimit,
goToBottom
} = useLobbyChat();
useEffect(() => { useEffect(() => {
//DashboardRoutes.preload(); //DashboardRoutes.preload();
//PublicRoutes.preload(); //PublicRoutes.preload();
}, []); }, []);
const [visibilityState, setVisibilityState] = useState('visible');
useEffect(() => {
setVisibilityState(document.visibilityState);
}, [document.visibilityState]);
const dispatch = useDispatch(); const dispatch = useDispatch();
const initJKScripts = () => { const initJKScripts = () => {
if(scriptLoaded.current){ if (scriptLoaded.current) {
return return;
} }
const app = window.JK.JamKazam(); const app = window.JK.JamKazam();
const jamServer = new window.JK.JamServer(app, function(event_type) { const jamServer = new window.JK.JamServer(app, function(event_type) {
console.log('EVENT_TYPE', event_type); console.log('EVENT_TYPE', event_type);
}); });
jamServer.initialize(); jamServer.initialize();
window.JK.initJamClient(app); window.JK.initJamClient(app);
const clientInit = new window.JK.ClientInit(); const clientInit = new window.JK.ClientInit();
clientInit.init(); clientInit.init();
@ -91,7 +108,7 @@ function JKDashboardMain() {
registerFriendRequestAccepted(); registerFriendRequestAccepted();
registerChatMessageCallback(); registerChatMessageCallback();
scriptLoaded.current = true scriptLoaded.current = true;
}; };
const registerTextMessageCallback = () => { const registerTextMessageCallback = () => {
@ -117,25 +134,43 @@ function JKDashboardMain() {
closeOnClick: true, closeOnClick: true,
pauseOnHover: true, pauseOnHover: true,
onClick: () => { onClick: () => {
setMessageUser({ id: msg.senderId, name: msg.senderName }) setMessageUser({ id: msg.senderId, name: msg.senderName });
setShowMessageModal(true) setShowMessageModal(true);
} }
}) });
handleNotification(payload, header.type); handleNotification(payload, header.type);
}); });
}; };
const registerChatMessageCallback = () => { const registerChatMessageCallback = () => {
window.JK.JamServer.registerMessageCallback(window.JK.MessageType.CHAT_MESSAGE, function (header, payload) { window.JK.JamServer.registerMessageCallback(window.JK.MessageType.CHAT_MESSAGE, function(header, payload) {
console.log("registerChatMessageCallback " + JSON.stringify(payload)); console.log('registerChatMessageCallback ' + JSON.stringify(payload));
// chatMessageReceived(payload); if ( payload !== undefined && payload.sender_id !== window.currentUser.id) {
// context.ChatActions.msgReceived(payload); if (visibilityState === 'hidden' && Notification.permission === 'granted') {
try{
// handledNotification(payload); const notification = new Notification("JamKazam Lobby Message", {
body: `${payload.sender_name}: ${truncate(payload.msg, 100)}`,
//icon: `${process.env.REACT_APP_CLIENT_BASE_URL}/assets/img/jamkazam-logo.png`
});
notification.onclick = function(event) {
event.preventDefault(); // prevent the browser from focusing the Notification's tab
window.focus();
event.target.close();
};
}catch(err){
console.log('Error when showing notification', err);
}
}
setChatMessages([]);
fetchLobbyMessages({
start: 0,
limit: lobbyChatOffset === 0 ? lobbyChatLimit : lobbyChatOffset * lobbyChatLimit
});
//goToBottom();
}
}); });
};
}
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) {
@ -197,14 +232,11 @@ function JKDashboardMain() {
<PrivateRoute path="/notifications" component={JKNotifications} /> <PrivateRoute path="/notifications" component={JKNotifications} />
{/*Redirect*/} {/*Redirect*/}
<Redirect to="/errors/404" /> <Redirect to="/errors/404" />
</Switch> </Switch>
{!isKanban && <Footer />} {!isKanban && <Footer />}
</div> </div>
{ messageUser && {messageUser && <JKMessageModal show={showMessageModal} setShow={setShowMessageModal} user={messageUser} />}
<JKMessageModal show={showMessageModal} setShow={setShowMessageModal} user={messageUser} />
}
{/* <SidePanelModal path={location.pathname} /> */} {/* <SidePanelModal path={location.pathname} /> */}
</div> </div>
); );

View File

@ -15,6 +15,7 @@ import useNativeAppCheck from '../../hooks/useNativeAppCheck';
import { useNativeApp } from '../../context/NativeAppContext'; import { useNativeApp } from '../../context/NativeAppContext';
import JKModalDialog from '../common/JKModalDialog'; import JKModalDialog from '../common/JKModalDialog';
import JKTooltip from '../common/JKTooltip'; import JKTooltip from '../common/JKTooltip';
import { updateUser } from '../../helpers/rest';
function JKMusicSessionsLobby() { function JKMusicSessionsLobby() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -31,24 +32,24 @@ function JKMusicSessionsLobby() {
const [submitted, setSubmitted] = useState(false); const [submitted, setSubmitted] = useState(false);
const [activeTab, setActiveTab] = useState('1'); const [activeTab, setActiveTab] = useState('1');
const [showNotificationsModal, setShowNotificationsModal] = useState(false); const [showNotificationsModal, setShowNotificationsModal] = useState(false);
const NOTIFICATION_INTERVAL = 1000 * 10 //10 seconds
useEffect(() => { useEffect(() => {
dispatch(fetchOnlineMusicians()); dispatch(fetchOnlineMusicians());
//check if browser notifications are enabled //check if browser notifications are enabled
try { try {
const notificationsEnabled = localStorage.getItem('showLobbyChatNotifications'); const notificationsEnabled = localStorage.getItem('showLobbyChatNotifications');
const dontAskAgain = localStorage.getItem('dontAskLobbyChatNotificationPermission'); const dontAskAgain = localStorage.getItem('dontAskLobbyChatNotificationPermission');
if (notificationsEnabled || dontAskAgain) { if (notificationsEnabled === 'true' || dontAskAgain === 'true') {
return; return;
} else { } else {
setTimeout(() => { setTimeout(() => {
if (true) { setShowNotificationsModal(true);
setShowNotificationsModal(true); }, NOTIFICATION_INTERVAL);
}
}, 10000);
} }
} catch (error) {} } catch (error) {
console.log('Error reading localStorage', error);
}
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -96,6 +97,7 @@ function JKMusicSessionsLobby() {
const grantShowNotificationsPermission = () => { const grantShowNotificationsPermission = () => {
setShowNotificationsModal(false); setShowNotificationsModal(false);
try { try {
if (!window.Notification) { if (!window.Notification) {
console.log('Your web browser does not support notifications.'); console.log('Your web browser does not support notifications.');
@ -109,10 +111,13 @@ function JKMusicSessionsLobby() {
Notification.requestPermission() Notification.requestPermission()
.then(function(p) { .then(function(p) {
if (p === 'granted') { if (p === 'granted') {
new Notification('Lobby Chat Notifications', { updateUser(currentUser.id, { accept_desktop_notifications: true }).then(() => {
body: 'Notifications will appear here.', console.log('User granted permission for desktop notifications');
new Notification('Lobby Chat Notifications', {
body: 'Notifications will appear here.',
});
localStorage.setItem('showLobbyChatNotifications', true);
}); });
localStorage.setItem('showLobbyChatNotifications', true);
} else { } else {
console.log('User blocked notifications.'); console.log('User blocked notifications.');
} }
@ -183,17 +188,16 @@ function JKMusicSessionsLobby() {
<Row className="swiper-container d-block d-md-none" data-testid="sessionsSwiper"> <Row className="swiper-container d-block d-md-none" data-testid="sessionsSwiper">
<Nav tabs> <Nav tabs>
<NavItem> <NavItem>
<NavLink className="active" onClick={() => setActiveTab('1')}> <NavLink style={{ cursor: 'pointer'}} className={ activeTab === '1' ? 'active' : null } onClick={() => setActiveTab('1')}>
Users Users
</NavLink> </NavLink>
</NavItem> </NavItem>
<NavItem> <NavItem>
<NavLink className="" onClick={() => setActiveTab('2')}> <NavLink style={{ cursor: 'pointer'}} className={ activeTab === '2' ? 'active' : null } onClick={() => setActiveTab('2')}>
Chat Chat
</NavLink> </NavLink>
</NavItem> </NavItem>
</Nav> </Nav>
<TabContent activeTab={activeTab}> <TabContent activeTab={activeTab}>
<TabPane tabId="1"> <TabPane tabId="1">
<Row> <Row>

View File

@ -191,7 +191,7 @@ const JKMessageModal = props => {
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button onClick={toggle}>Cancel</Button> <Button onClick={toggle}>Close</Button>
<Button color="primary" onClick={sendMessage} disabled={!newMessage}> <Button color="primary" onClick={sendMessage} disabled={!newMessage}>
Send Message Send Message
</Button> </Button>

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { useResponsive, useIsMobile } from '@farfetch/react-context-responsive'; import { useResponsive, useIsMobile } from '@farfetch/react-context-responsive';
import JKInstrumentIcon from './JKInstrumentIcon'; import JKInstrumentIcon from './JKInstrumentIcon';
const JKPersonInstrumentsList = ({ instruments, showAll, toggleMoreDetails }) => { const JKPersonInstrumentsList = ({ instruments, showIcons, showAll, toggleMoreDetails }) => {
const proficiencies = { const proficiencies = {
'1': 'Beginner', '1': 'Beginner',
'2': 'Intermediate', '2': 'Intermediate',
@ -26,9 +26,11 @@ const JKPersonInstrumentsList = ({ instruments, showAll, toggleMoreDetails }) =>
{instrumentsToShow && {instrumentsToShow &&
instrumentsToShow.map(instrument => ( instrumentsToShow.map(instrument => (
<div key={instrument.instrument_id} className="text-nowrap mb-1"> <div key={instrument.instrument_id} className="text-nowrap mb-1">
<span className='mr-1'> {showIcons && (
<span className="mr-1">
<JKInstrumentIcon instrumentId={instrument.instrument_id} instrumentName={instrument.description} /> <JKInstrumentIcon instrumentId={instrument.instrument_id} instrumentName={instrument.description} />
</span> </span>
)}
<strong>{instrument.description}:</strong> {proficiencies[instrument.proficiency_level]} <strong>{instrument.description}:</strong> {proficiencies[instrument.proficiency_level]}
</div> </div>
))} ))}
@ -44,11 +46,13 @@ const JKPersonInstrumentsList = ({ instruments, showAll, toggleMoreDetails }) =>
JKPersonInstrumentsList.propTypes = { JKPersonInstrumentsList.propTypes = {
instruments: PropTypes.arrayOf(PropTypes.object).isRequired, instruments: PropTypes.arrayOf(PropTypes.object).isRequired,
showAll: PropTypes.bool, showAll: PropTypes.bool,
showIcons: PropTypes.bool,
toggleMoreDetails: PropTypes.func toggleMoreDetails: PropTypes.func
}; };
JKPersonInstrumentsList.defaltProps = { JKPersonInstrumentsList.defaltProps = {
showAll: false showAll: false,
showIcons: true
}; };
export default JKPersonInstrumentsList; export default JKPersonInstrumentsList;

View File

@ -12,6 +12,8 @@ import { useResponsive } from '@farfetch/react-context-responsive';
import useOnScreen from '../../hooks/useOnScreen'; import useOnScreen from '../../hooks/useOnScreen';
import useKeepScrollPosition from '../../hooks/useKeepScrollPosition'; import useKeepScrollPosition from '../../hooks/useKeepScrollPosition';
import { useLobbyChat } from './JKLobbyChatContext';
function JKLobbyChat() { function JKLobbyChat() {
const CHANNEL_LOBBY = 'lobby'; const CHANNEL_LOBBY = 'lobby';
const LIMIT = 10; const LIMIT = 10;
@ -22,48 +24,50 @@ function JKLobbyChat() {
const { greaterThan } = useResponsive(); const { greaterThan } = useResponsive();
const scrolledToBottom = useRef(false); const scrolledToBottom = useRef(false);
const { currentUser } = useAuth(); const { currentUser } = useAuth();
const [fetching, setFetching] = useState(false); //const [fetching, setFetching] = useState(false);
const [messagesArrived, setMessagesArrived] = useState(false); const [messagesArrived, setMessagesArrived] = useState(false);
const [offset, setOffset] = useState(0); //const [offset, setOffset] = useState(0);
const chatMessages = useSelector(state => state.lobbyChat.records.messages); const chatMessages = useSelector(state => state.lobbyChat.records.messages);
const next = useSelector(state => state.lobbyChat.records.next); const next = useSelector(state => state.lobbyChat.records.next);
const createStatus = useSelector(state => state.lobbyChat.create_status); const createStatus = useSelector(state => state.lobbyChat.create_status);
const [messages, setMessages] = useState([]); //const [messages, setMessages] = useState([]);
const [lastMessageRef, setLastMessageRef] = useState(null); const [lastMessageRef, setLastMessageRef] = useState(null);
const isIntersecting = useOnScreen({ current: lastMessageRef }); const isIntersecting = useOnScreen({ current: lastMessageRef });
const { containerRef } = useKeepScrollPosition([messages]); //const { containerRef } = useKeepScrollPosition([messages]);
const fetchMessages = async (overrides = {}) => { const { messages, setMessages, lobbyChatOffset, setLobbyChatOffset, fetchLobbyMessages, containerRef, goToBottom } = useLobbyChat();
const options = { start: offset * LIMIT, limit: LIMIT };
const params = { ...options, ...overrides }; // const fetchMessages = async (overrides = {}) => {
try { // const options = { start: lobbyChatOffset * LIMIT, limit: LIMIT };
setFetching(true); // const params = { ...options, ...overrides };
await dispatch(fetchLobbyChatMessages(params)).unwrap(); // try {
} catch (error) { // setLobbyChatFetching(true);
console.log('Error when fetching chat messages', error); // await dispatch(fetchLobbyChatMessages(params)).unwrap();
} finally { // } catch (error) {
setFetching(false); // console.log('Error when fetching chat messages', error);
} // } finally {
}; // setLobbyChatFetching(false);
// }
// };
useEffect(() => { useEffect(() => {
if (isIntersecting) { if (isIntersecting) {
if (next) { if (next) {
setOffset(prev => prev + 1); setLobbyChatOffset(prev => prev + 1);
} }
} }
}, [isIntersecting]); }, [isIntersecting]);
useEffect(() => { useEffect(() => {
if (offset !== 0) { if (lobbyChatOffset !== 0) {
fetchMessages(); fetchLobbyMessages();
} }
}, [offset]); }, [lobbyChatOffset]);
useEffect(() => { useEffect(() => {
fetchMessages(); fetchLobbyMessages();
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -91,9 +95,9 @@ function JKLobbyChat() {
return ht == 0 || st == sh - ht; return ht == 0 || st == sh - ht;
}; };
const goToBottom = () => { // const goToBottom = () => {
containerRef.current.scrollTop = containerRef.current.scrollHeight; // 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') {
@ -129,9 +133,8 @@ function JKLobbyChat() {
useEffect(() => { useEffect(() => {
if (createStatus === 'succeeded') { if (createStatus === 'succeeded') {
// fetchMessages({ start: 0, limit: 1, lastOnly: true });
setMessages([]) setMessages([])
fetchMessages({ start: 0, limit: offset === 0 ? LIMIT : offset * LIMIT}) fetchLobbyMessages({ start: 0, limit: lobbyChatOffset === 0 ? LIMIT : lobbyChatOffset * LIMIT})
messageTextBox.current.focus(); messageTextBox.current.focus();
} }
}, [createStatus]); }, [createStatus]);
@ -194,7 +197,7 @@ function JKLobbyChat() {
</div> </div>
)} )}
</div> </div>
<div className="mt-2" style={{ height: '20%' }}> <div className="mt-2 mb-2" style={{ height: '20%' }}>
<textarea <textarea
style={{ width: '100%' }} style={{ width: '100%' }}
value={newMessage} value={newMessage}
@ -214,3 +217,4 @@ function JKLobbyChat() {
} }
export default JKLobbyChat; export default JKLobbyChat;

View File

@ -0,0 +1,42 @@
import React, { createContext, useState } from 'react';
import { useDispatch } from 'react-redux';
import { fetchLobbyChatMessages } from '../../store/features/lobbyChatMessagesSlice';
import useKeepScrollPosition from '../../hooks/useKeepScrollPosition';
// Create the context
export const JKLobbyChatContext = createContext();
// Create a provider component
export const JKLobbyChatProvider = ({ children }) => {
const dispatch = useDispatch();
const LIMIT = 10;
const [messages, setMessages] = useState([]);
const [lobbyChatLimit, setLobbyChatLimit] = useState(LIMIT);
const [lobbyChatOffset, setLobbyChatOffset] = useState(0);
const [lobbyChatFetching, setLobbyChatFetching] = useState(false);
const { containerRef } = useKeepScrollPosition([messages]);
const fetchLobbyMessages = async (overrides = {}) => {
const options = { start: lobbyChatOffset * LIMIT, limit: LIMIT };
const params = { ...options, ...overrides };
try {
setLobbyChatFetching(true);
await dispatch(fetchLobbyChatMessages(params)).unwrap();
} catch (error) {
console.log('Error when fetching chat messages', error);
} finally {
setLobbyChatFetching(false);
}
};
const goToBottom = () => {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
};
return (
<JKLobbyChatContext.Provider value={{ messages, setMessages, lobbyChatOffset, setLobbyChatOffset, fetchLobbyMessages, lobbyChatLimit, goToBottom, containerRef }}>
{children}
</JKLobbyChatContext.Provider>
);
};
export const useLobbyChat = () => React.useContext(JKLobbyChatContext);

View File

@ -5,6 +5,7 @@ import JKMessageButton from '../profile/JKMessageButton';
import JKMoreDetailsButton from '../profile/JKMoreDetailsButton'; import JKMoreDetailsButton from '../profile/JKMoreDetailsButton';
import JKLatencyBadge from '../profile/JKLatencyBadge'; import JKLatencyBadge from '../profile/JKLatencyBadge';
import JKProfileInstrumentsList from '../profile/JKProfileInstrumentsList'; import JKProfileInstrumentsList from '../profile/JKProfileInstrumentsList';
import JKProfileGenres from '../profile/JKProfileGenres';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import JKProfileSidePanel from '../profile/JKProfileSidePanel'; import JKProfileSidePanel from '../profile/JKProfileSidePanel';
@ -66,14 +67,56 @@ function JKLobbyUser({ user, setSelectedUsers }) {
<JKLatencyBadge latencyData={latencyData} /> <JKLatencyBadge latencyData={latencyData} />
</div> </div>
<div> <div>
<strong>{t('person_attributes.instruments', { ns: 'people' })}</strong> <JKProfileInstrumentsList
{/* <JKProfileInstrumentsList instruments={user.instruments} toggleMoreDetails={toggleMoreDetails} /> */} instruments={user.instruments}
toggleMoreDetails={toggleMoreDetails}
showIcons={false}
/>
</div> </div>
</div> </div>
</div> </div>
</td> </td>
<td className="align-middle text-center"> <td className="align-middle text-center">
<div className="d-inline-flex flex-wrap" style={{ gap: '2px' }}>
<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>
</div>
</td>
</tr>
) : (
<>
<div>
<strong>{t('person_attributes.latency_to_me', { ns: 'people' })}:</strong>{' '}
<JKLatencyBadge latencyData={latencyData} />
</div>
<div className="mt-3">
<h5>{t('person_attributes.instruments', { ns: 'people' })}</h5>
<JKProfileInstrumentsList
instruments={user.instruments}
toggleMoreDetails={toggleMoreDetails}
showIcons={false}
/>
</div>
<div className="mt-3">
<h5>{t('person_attributes.genres', { ns: 'people' })}</h5>
<JKProfileGenres genres={user.genres} toggleMoreDetails={toggleMoreDetails} />
</div>
<br />
<div className="d-inline-flex flex-wrap" style={{ gap: '2px' }}>
<JKConnectButton <JKConnectButton
currentUser={currentUser} currentUser={currentUser}
user={user} user={user}
@ -86,48 +129,16 @@ function JKLobbyUser({ user, setSelectedUsers }) {
<FontAwesomeIcon icon="comments" transform="shrink-4 down-1" className="mr-1" /> <FontAwesomeIcon icon="comments" transform="shrink-4 down-1" className="mr-1" />
</JKMessageButton> </JKMessageButton>
<JKMoreDetailsButton toggleMoreDetails={toggleMoreDetails} cssClasses="btn btn-primary fs--1 px-2 py-1"> <a href="/#" onClick={toggleMoreDetails} data-testid="btnMore">
<FontAwesomeIcon icon="user" transform="shrink-4 down-1" className="mr-1" /> <span
</JKMoreDetailsButton> className="btn btn-primary fs--1 px-2 py-1"
</td> data-bs-toggle="tooltip"
</tr> title={t('view_profile', { ns: 'people' })}
) : ( >
<> <FontAwesomeIcon icon="user" transform="shrink-4 down-1" className="mr-1" />
<div> </span>
<strong>{t('person_attributes.latency_to_me', { ns: 'people' })}:</strong>{' '} </a>
<JKLatencyBadge latencyData={latencyData} />
</div> </div>
<div>
<h5>{t('person_attributes.instruments', { ns: 'people' })}</h5>
{/* <JKProfileInstrumentsList instruments={instruments} toggleMoreDetails={toggleMoreDetails} /> */}
</div>
<div>
<h5>{t('person_attributes.genres', { ns: 'people' })}</h5>
{/* <JKProfileGenres genres={genres} toggleMoreDetails={toggleMoreDetails} /> */}
</div>
<br />
<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>
<a href="/#" onClick={toggleMoreDetails} data-testid="btnMore">
<span
className="btn btn-primary fs--1 px-2 py-1"
data-bs-toggle="tooltip"
title={t('view_profile', { ns: 'people' })}
>
<FontAwesomeIcon icon="user" transform="shrink-4 down-1" className="mr-1" />
</span>
</a>
</> </>
)} )}

View File

@ -29,7 +29,7 @@ function JKLobbyUserList({ loadingStatus, onlineMusicians, selectedUsers, setSel
onlineMusicians.map(musician => <JKLobbyUser key={musician.id} user={musician} setSelectedUsers={setSelectedUsers} />) onlineMusicians.map(musician => <JKLobbyUser key={musician.id} user={musician} setSelectedUsers={setSelectedUsers} />)
) : ( ) : (
<tr> <tr>
<td colSpan={2}>No users currently online.</td> <td colSpan={2}>No musicians are currently available here.</td>
</tr> </tr>
)} )}
</tbody> </tbody>

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import JKProfileAvatar from '../profile/JKProfileAvatar'; import JKProfileAvatar from '../profile/JKProfileAvatar';
import { isIterableArray } from '../../helpers/utils';
import Loader from '../common/Loader';
// import Swiper core and required modules // import Swiper core and required modules
import SwiperCore, { Navigation, Pagination, Scrollbar, A11y } from 'swiper'; import SwiperCore, { Navigation, Pagination, Scrollbar, A11y } from 'swiper';
@ -23,53 +25,64 @@ SwiperCore.use([Navigation, Pagination, Scrollbar, A11y]);
const JKLobbyUserSwiper = ({ onlineMusicians, setSelectedUsers, loadingStatus }) => { const JKLobbyUserSwiper = ({ onlineMusicians, setSelectedUsers, loadingStatus }) => {
return ( return (
<> <>
<Swiper {loadingStatus === 'loading' && onlineMusicians.length === 0 ? (
spaceBetween={0} <Loader />
slidesPerView={1} ) : (
//onSlideChange={() => console.log('slide change')} <>
onSlideNextTransitionEnd={swiper => { {isIterableArray(onlineMusicians) ? (
if(swiper.isEnd){ <>
//goNextPage() <Swiper
} spaceBetween={0}
}} slidesPerView={1}
pagination={{ //onSlideChange={() => console.log('slide change')}
clickable: true, onSlideNextTransitionEnd={swiper => {
type: 'custom' if (swiper.isEnd) {
}} //goNextPage()
navigation={{ }
nextEl: '.swiper-button-next', }}
prevEl: '.swiper-button-prev' pagination={{
}} clickable: true,
> type: 'custom'
{onlineMusicians.map((musician, index) => ( }}
navigation={{
<SwiperSlide key={`session-lobby-swiper-item-${musician.id}`}> nextEl: '.swiper-button-next',
<Card className="swiper-person-card"> prevEl: '.swiper-button-prev'
<CardHeader className="text-center bg-200"> }}
<div className="avatar avatar-xl d-inline-block me-2 mr-2"> >
<JKProfileAvatar url={musician.photo_url} size="xl"/> {onlineMusicians.map((musician, index) => (
</div> <SwiperSlide key={`session-lobby-swiper-item-${musician.id}`}>
<h5 className="d-inline-block align-top mt-1">{musician.name}</h5> <Card className="swiper-person-card">
</CardHeader> <CardHeader className="bg-200">
<CardBody> <div className="avatar avatar-xl d-inline-block me-2 mr-2">
<JKLobbyUser user={musician} setSelectedUsers={setSelectedUsers} viewMode="swipe" /> <JKProfileAvatar url={musician.photo_url} size="xl" />
</CardBody> </div>
</Card> <h5 className="d-inline-block align-top mt-1">{musician.name}</h5>
</SwiperSlide> </CardHeader>
))} <CardBody>
</Swiper> <JKLobbyUser user={musician} setSelectedUsers={setSelectedUsers} viewMode="swipe" />
<div className="py-4 px-6 bg-white border-top w-100 fixed-bottom"> </CardBody>
<div className="swiper-pagination" /> </Card>
<div className="swiper-button-prev" /> </SwiperSlide>
<div className="swiper-button-next" /> ))}
</div> </Swiper>
<div className="py-4 px-6 bg-white border-top w-100 fixed-bottom">
<div className="swiper-pagination" />
<div className="swiper-button-prev" />
<div className="swiper-button-next" />
</div>
</>
) : (
<div>No musicians are currently available here.</div>
)}
</>
)}
</> </>
); );
}; };
JKLobbyUserSwiper.propTypes = { JKLobbyUserSwiper.propTypes = {
onlineMusicians: PropTypes.arrayOf(PropTypes.instanceOf(Object)).isRequired, onlineMusicians: PropTypes.arrayOf(PropTypes.instanceOf(Object)).isRequired,
setSelectedUsers: PropTypes.func.isRequired, setSelectedUsers: PropTypes.func.isRequired
}; };
export default JKLobbyUserSwiper; export default JKLobbyUserSwiper;

View File

@ -4,7 +4,7 @@ import { useLocation } from "react-router-dom";
const BrowserQueryContext = React.createContext(null) const BrowserQueryContext = React.createContext(null)
export const BrowserQueryProvider = ({children}) => { export const BrowserQueryProvider = ({children}) => {
function useQuery() { function useQuery() {
return new URLSearchParams(useLocation().search); return new URLSearchParams(useLocation().search);
} }

View File

@ -43,6 +43,25 @@ export const getPeopleIndex = () => {
}) })
} }
export const getLobbyUsers = () => {
return new Promise((resolve, reject) => {
apiFetch(`/users/lobby`)
.then(response => resolve(response))
.catch(error => reject(error))
})
}
export const updateUser = (id, data) => {
return new Promise((resolve, reject) => {
apiFetch(`/users/${id}`, {
method: 'POST',
body: JSON.stringify(data)
})
.then(response => resolve(response))
.catch(error => reject(error))
})
}
export const getGenres = () => { export const getGenres = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
apiFetch('/genres') apiFetch('/genres')

View File

@ -7,6 +7,8 @@ import { BrowserQueryProvider } from '../context/BrowserQuery';
import { NativeAppProvider } from '../context/NativeAppContext'; import { NativeAppProvider } from '../context/NativeAppContext';
import { JKLobbyChatProvider } from '../components/sessions/JKLobbyChatContext';
const DashboardLayout = ({ location }) => { const DashboardLayout = ({ location }) => {
useEffect(() => { useEffect(() => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
@ -16,7 +18,9 @@ const DashboardLayout = ({ location }) => {
<UserAuth path={location.pathname}> <UserAuth path={location.pathname}>
<BrowserQueryProvider> <BrowserQueryProvider>
<NativeAppProvider> <NativeAppProvider>
<DashboardMain /> <JKLobbyChatProvider>
<DashboardMain />
</JKLobbyChatProvider>
</NativeAppProvider> </NativeAppProvider>
</BrowserQueryProvider> </BrowserQueryProvider>
</UserAuth> </UserAuth>

View File

@ -43,6 +43,18 @@ const chatMessagesSlice = createSlice({
}; };
state.status = 'succeeded'; state.status = 'succeeded';
}) })
// .addCase(fetchLobbyChatMessages.fulfilled, (state, action) => {
// const lastOnly = action.meta.arg.lastOnly;
// const currentChatMessageIds = state.records.messages.map(m => m.id);
// const newMessages = action.payload.chats.filter(m => !currentChatMessageIds.includes(m.id));
// state.records = {
// next: state.records.next === null && lastOnly? null : action.payload.next,
// messages: state.records.messages.concat(newMessages).map(m => ({...m, status: 'delivered'})).sort((a, b) => {
// return new Date(a.created_at) - new Date(b.created_at);
// })
// };
// 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';

View File

@ -1,5 +1,5 @@
import {createSlice, createAsyncThunk} from "@reduxjs/toolkit"; import {createSlice, createAsyncThunk} from "@reduxjs/toolkit";
import { getPeopleIndex } from "../../helpers/rest"; import { getPeopleIndex, getLobbyUsers } from "../../helpers/rest";
const initialState = { const initialState = {
musicians: [], musicians: [],
@ -10,7 +10,8 @@ const initialState = {
export const fetchOnlineMusicians = createAsyncThunk( export const fetchOnlineMusicians = createAsyncThunk(
'onlineMusician/fetchMusicians', 'onlineMusician/fetchMusicians',
async (options, thunkAPI) => { async (options, thunkAPI) => {
const response = await getPeopleIndex(options) //const response = await getPeopleIndex(options)
const response = await getLobbyUsers(options)
return response.json() return response.json()
} }
) )
@ -32,6 +33,7 @@ export const onlineMusiciansSlice = createSlice({
state.status = 'loading' state.status = 'loading'
}) })
.addCase(fetchOnlineMusicians.fulfilled, (state, action) => { .addCase(fetchOnlineMusicians.fulfilled, (state, action) => {
console.log('fetchOnlineMusicians.fulfilled', action.payload)
state.status = 'succeeded' state.status = 'succeeded'
state.musicians = action.payload state.musicians = action.payload
}) })

View File

@ -0,0 +1,8 @@
class AddAcceptDesktopNotificationsToUsers < ActiveRecord::Migration
def self.up
execute("ALTER TABLE public.users ADD COLUMN accept_desktop_notifications BOOLEAN DEFAULT false;")
end
def self.down
execute("ALTER TABLE public.users DROP COLUMN accept_desktop_notifications;")
end
end

View File

@ -187,7 +187,7 @@ module JamRuby
elsif channel == 'global' elsif channel == 'global'
@@mq_router.publish_to_active_clients(msg) @@mq_router.publish_to_active_clients(msg)
elsif channel == 'lobby' elsif channel == 'lobby'
@@mq_router.publish_to_all_clients(msg) #TODO: only publish to lobby users @@mq_router.publish_to_active_clients(msg) #TODO: only publish to lobby users
elsif channel == 'lesson' elsif channel == 'lesson'
@@mq_router.publish_to_user(target_user.id, msg, sender = {:client_id => client_id}) if target_user @@mq_router.publish_to_user(target_user.id, msg, sender = {:client_id => client_id}) if target_user
@@mq_router.publish_to_user(user.id, msg, sender = {:client_id => client_id}) if user @@mq_router.publish_to_user(user.id, msg, sender = {:client_id => client_id}) if user

View File

@ -52,6 +52,8 @@ module JamRuby
tm.message = sanitized_text tm.message = sanitized_text
tm.target_user_id = target_user_id tm.target_user_id = target_user_id
tm.source_user_id = source_user_id tm.source_user_id = source_user_id
tm.valid?
Rails.logger.info "___ERROR #{tm.errors.inspect}" unless tm.errors.empty?
tm.save tm.save
# send notification # send notification

View File

@ -21,6 +21,7 @@ module JamRuby
JAM_REASON_JOIN = 'j' JAM_REASON_JOIN = 'j'
JAM_REASON_IMPORT = 'i' JAM_REASON_IMPORT = 'i'
JAM_REASON_LOGIN = 'l' JAM_REASON_LOGIN = 'l'
JAM_REASON_PRESENT = 'p'
# MOD KEYS # MOD KEYS
MOD_GEAR = "gear" MOD_GEAR = "gear"
@ -308,6 +309,14 @@ module JamRuby
scope :came_through_amazon, -> { joins(:posa_cards).where('posa_cards.lesson_package_type_id in (?)', LessonPackageType::AMAZON_PACKAGES + LessonPackageType::LESSON_PACKAGE_TYPES)} scope :came_through_amazon, -> { joins(:posa_cards).where('posa_cards.lesson_package_type_id in (?)', LessonPackageType::AMAZON_PACKAGES + LessonPackageType::LESSON_PACKAGE_TYPES)}
scope :not_deleted, ->{ where(deleted: false) } scope :not_deleted, ->{ where(deleted: false) }
def self.lobby(current_user, options = {})
query = User.where("users.id <> ? AND users.last_jam_updated_at > ?", current_user.id, 15.minutes.ago)
if live_music_sessions = ActiveMusicSession.count > 0
query = query.where("users.id NOT IN (?)", live_music_sessions.pluck(:user_id))
end
query
end
def after_save def after_save
if school_interest && !school_interest_was if school_interest && !school_interest_was
if education_interest if education_interest
@ -1959,21 +1968,28 @@ module JamRuby
def update_addr_loc(connection, reason) def update_addr_loc(connection, reason)
unless connection unless connection
@@log.warn("no connection specified in update_addr_loc with reason #{reason}") @@log.warn("no connection specified in update_addr_loc with reason #{reason}")
update_last_active(reason)
return return
end end
if connection.locidispid.nil? if connection.locidispid.nil?
@@log.warn("no locidispid for connection's ip_address: #{connection.ip_address}") @@log.warn("no locidispid for connection's ip_address: #{connection.ip_address}")
update_last_active(reason)
return return
end end
# we don't use a websocket login to update the user's record unless there is no addr # we don't use a websocket login to update the user's record unless there is no addr
if reason == JAM_REASON_LOGIN && last_jam_addr if reason == JAM_REASON_LOGIN && last_jam_addr
update_last_active(reason)
return return
end end
self.last_jam_addr = connection.addr self.last_jam_addr = connection.addr
self.last_jam_locidispid = connection.locidispid self.last_jam_locidispid = connection.locidispid
update_last_active(reason)
end
def update_last_active(reason)
self.last_jam_updated_reason = reason self.last_jam_updated_reason = reason
self.last_jam_updated_at = Time.now self.last_jam_updated_at = Time.now
unless self.save unless self.save

View File

@ -16,6 +16,7 @@
context.WEB_SOCKET_SWF_LOCATION = "assets/flash/WebSocketMain.swf"; context.WEB_SOCKET_SWF_LOCATION = "assets/flash/WebSocketMain.swf";
context.JK.JamServer = function (app, activeElementEvent) { context.JK.JamServer = function (app, activeElementEvent) {
console.log("_DEBUG Init JK scripts....", context.JK.JamServer);
// uniquely identify the websocket connection // uniquely identify the websocket connection
var channelId = null; var channelId = null;
var clientType = null; var clientType = null;
@ -197,11 +198,11 @@
function loggedIn(header, payload) { function loggedIn(header, payload) {
console.log("___loggedIn", header, payload)
// reason for setTimeout: // reason for setTimeout:
// loggedIn causes an absolute ton of initialization to happen, and errors sometimes happen // loggedIn causes an absolute ton of initialization to happen, and errors sometimes happen
// but because loggedIn(header,payload) is a callback from a websocket, the browser doesn't show a stack trace... // but because loggedIn(header,payload) is a callback from a websocket, the browser doesn't show a stack trace...
setTimeout(function() { setTimeout(function() {
server.signedIn = true; server.signedIn = true;
server.clientID = payload.client_id; server.clientID = payload.client_id;
@ -217,7 +218,7 @@
heartbeatStateReset(); heartbeatStateReset();
app.clientId = payload.client_id; app.clientId = payload.client_id;
if (isClientMode() && context.jamClient) { if (isClientMode() && context.jamClient) {
// tell the backend that we have logged in // tell the backend that we have logged in
try { try {
@ -247,6 +248,7 @@
// where there is no device on startup for the current profile. // where there is no device on startup for the current profile.
// So, in that case, it's possible that a reconnect loop will attempt, but we *do not want* // So, in that case, it's possible that a reconnect loop will attempt, but we *do not want*
// it to go through unless we've passed through .OnLoggedIn // it to go through unless we've passed through .OnLoggedIn
server.connected = true; server.connected = true;
server.reconnecting = false; server.reconnecting = false;
server.connecting = false; server.connecting = false;
@ -315,17 +317,18 @@
} }
function activityCheck() { function activityCheck() {
var timeoutTime = 300000; // 5 * 1000 * 60 , 5 minutes //var timeoutTime = 300000; // 5 * 1000 * 60 , 5 minutes
var timeoutTime = 2000; // 5 * 1000 * 60 , 5 minutes
active = true; active = true;
setActive(active) setActive(active)
activityTimeout = setTimeout(markAway, timeoutTime); activityTimeout = setTimeout(markAway, timeoutTime);
$(document).ready(function() { $(document).ready(function() {
$('body').bind('mousedown keydown touchstart focus', function(event) { $('body').bind('mousedown keydown touchstart focus', function(event) {
if (activityTimeout) { if (activityTimeout) {
clearTimeout(activityTimeout); clearTimeout(activityTimeout);
activityTimeout = null; activityTimeout = null;
} }
if (!active) { if (!active) {
if(server && server.connected) { if(server && server.connected) {
logger.debug("awake again!") logger.debug("awake again!")
@ -701,6 +704,7 @@
server.socket.onopen = server.onOpen; server.socket.onopen = server.onOpen;
server.socket.onmessage = server.onMessage; server.socket.onmessage = server.onMessage;
server.socket.onclose = server.onClose; server.socket.onclose = server.onClose;
server.socket.onerror = server.onError;
connectTimeout = setTimeout(function () { connectTimeout = setTimeout(function () {
logger.debug("connection timeout fired") logger.debug("connection timeout fired")
@ -710,7 +714,7 @@
server.close(true); server.close(true);
connectDeferred.reject(); connectDeferred.reject();
} }
}, 4000); }, 10000);
return connectDeferred; return connectDeferred;
}; };
@ -740,15 +744,16 @@
server.onOpen = function () { server.onOpen = function () {
logger.debug("server.onOpen"); logger.debug("server.onOpen");
// we should receive LOGIN_ACK very soon. we already set a timer elsewhere to give 4 seconds to receive it // we should receive LOGIN_ACK very soon. we already set a timer elsewhere to give 10 seconds to receive it
}; };
server.onMessage = function (e) { server.onMessage = function (e) {
console.log("server.onMessage", e)
var message = JSON.parse(e.data), var message = JSON.parse(e.data),
messageType = message.type.toLowerCase(), messageType = message.type.toLowerCase(),
payload = message[messageType], payload = message[messageType],
callbacks = server.dispatchTable[message.type]; callbacks = server.dispatchTable[message.type];
console.log("server.onMessage", message, messageType, payload, callbacks)
if (message.type == context.JK.MessageType.PEER_MESSAGE) { if (message.type == context.JK.MessageType.PEER_MESSAGE) {
console.log("server.onMessage:" + messageType); console.log("server.onMessage:" + messageType);
} }
@ -773,8 +778,8 @@
}; };
// onClose is called if either client or server closes connection // onClose is called if either client or server closes connection
server.onClose = function () { server.onClose = function (e) {
logger.info("Socket to server closed."); logger.info("Socket to server closed.", e.code, e.reason);
var disconnectedSocket = this; var disconnectedSocket = this;
@ -790,6 +795,10 @@
closedCleanup(true); closedCleanup(true);
}; };
server.onError = function (e) {
logger.error("Socket error.", e);
};
server.send = function (message) { server.send = function (message) {
var jsMessage = JSON.stringify(message); var jsMessage = JSON.stringify(message);
@ -942,9 +951,7 @@
registerServerRejection(); registerServerRejection();
registerSocketClosed(); registerSocketClosed();
activityCheck(); activityCheck();
console.log("Init JK scripts....", context.JK.JamServer);
context.JK.JamServer.send("hello") context.JK.JamServer.send("hello")
// $inSituBanner = $('.server-connection'); // $inSituBanner = $('.server-connection');

View File

@ -25,6 +25,11 @@ class ApiUsersController < ApiController
@users = User.paginate(page: params[:page]) @users = User.paginate(page: params[:page])
respond_with @users, responder: ApiResponder, :status => 200 respond_with @users, responder: ApiResponder, :status => 200
end end
def lobby
@users = User.lobby(current_user)
respond_with @users, responder: ApiResponder, :status => 200
end
def calendar def calendar
#@user=lookup_user #@user=lookup_user
@ -230,6 +235,7 @@ class ApiUsersController < ApiController
@user.education_interest = !!params[:education_interest] @user.education_interest = !!params[:education_interest]
@user.retailer_interest = !!params[:retailer_interest] @user.retailer_interest = !!params[:retailer_interest]
@user.desired_package = LessonPackageType.find_by_package_type!(params[:desired_package]) if params.has_key?(:desired_package) @user.desired_package = LessonPackageType.find_by_package_type!(params[:desired_package]) if params.has_key?(:desired_package)
@user.accept_desktop_notifications = params[:accept_desktop_notifications] if params.has_key?(:accept_desktop_notifications)
if @user.save if @user.save
test_drive_package_details = params[:test_drive_package] test_drive_package_details = params[:test_drive_package]

View File

@ -0,0 +1,12 @@
collection @users
# do not retrieve all child collections when showing a list of users
attributes :id, :first_name, :last_name, :name, :photo_url, :biography
child :musician_instruments => :instruments do
attributes :instrument_id, :description, :proficiency_level, :priority
end
child :genres => :genres do
attributes :genre_id, :description
end

View File

@ -391,6 +391,7 @@ Rails.application.routes.draw do
match '/me' => 'api_users#me', :via => :get match '/me' => 'api_users#me', :via => :get
match '/users' => 'api_users#index', :via => :get match '/users' => 'api_users#index', :via => :get
match '/users/lobby' => 'api_users#lobby', :via => :get
match '/users' => 'api_users#create', :via => :post match '/users' => 'api_users#create', :via => :post
match '/users/:id' => 'api_users#show', :via => :get, :as => 'api_user_detail' match '/users/:id' => 'api_users#show', :via => :get, :as => 'api_user_detail'
match '/users/:id/authorizations' => 'api_users#authorizations', :via => :get match '/users/:id/authorizations' => 'api_users#authorizations', :via => :get

View File

@ -95,7 +95,8 @@ module JamWebsockets
# this thread runs forever while WSG runs, and should do anything easily gotten out of critical message handling path # this thread runs forever while WSG runs, and should do anything easily gotten out of critical message handling path
@background_thread = Thread.new { @background_thread = Thread.new {
count = 0 client_check_count = 0
user_last_seen_count = 0
while true while true
@ -103,11 +104,17 @@ module JamWebsockets
periodical_check_connections periodical_check_connections
periodical_notification_seen periodical_notification_seen
if count == 30 if client_check_count == 30
periodical_check_clients periodical_check_clients
count = 0 client_check_count = 0
end end
count = count + 1 client_check_count = client_check_count + 1
if user_last_seen_count == 120
periodical_update_user_last_seen
user_last_seen_count = 0
end
user_last_seen_count = user_last_seen_count + 1
rescue => e rescue => e
@log.error "unhandled error in thread #{e}" @log.error "unhandled error in thread #{e}"
@ -1514,6 +1521,21 @@ module JamWebsockets
end end
def periodical_update_user_last_seen
active_users_ids = @client_lookup.map { |client_id, client_context| client_context.active ? client_context.user.id : nil }.compact.uniq
if active_users_ids.any?
sql = %{
update users set last_jam_updated_at = now(), last_jam_updated_reason=#{User::JAM_REASON_PRESENT} where users.id in (#{active_users_ids.join(',')});
}
@log.info("SQL #{sql}")
ConnectionManager.active_record_transaction do |connection_manager, conn|
conn.exec(sql)
end
end
end
def periodical_stats_dump def periodical_stats_dump
# assume 60 seconds per status dump # assume 60 seconds per status dump