-
-
-
-
-
- ) : (
- <>
-
-
{t('person_attributes.latency_to_me', { ns: 'people' })}:{' '}
-
+
+
+
+
+
-
-
{t('person_attributes.instruments', { ns: 'people' })}
- {/* */}
-
-
-
{t('person_attributes.genres', { ns: 'people' })}
- {/* */}
-
-
-
-
}
- removeContent={
}
- cssClasses="fs--1 px-2 py-1 mr-1"
- />
-
-
-
-
-
-
-
-
-
-
>
)}
diff --git a/jam-ui/src/components/sessions/JKLobbyUserList.js b/jam-ui/src/components/sessions/JKLobbyUserList.js
index 267830d5f..3cbbe71d0 100644
--- a/jam-ui/src/components/sessions/JKLobbyUserList.js
+++ b/jam-ui/src/components/sessions/JKLobbyUserList.js
@@ -29,7 +29,7 @@ function JKLobbyUserList({ loadingStatus, onlineMusicians, selectedUsers, setSel
onlineMusicians.map(musician =>
)
) : (
- | No users currently online. |
+ No musicians are currently available here. |
)}
diff --git a/jam-ui/src/components/sessions/JKLobbyUserSwiper.js b/jam-ui/src/components/sessions/JKLobbyUserSwiper.js
index 62d22b557..f573ef118 100644
--- a/jam-ui/src/components/sessions/JKLobbyUserSwiper.js
+++ b/jam-ui/src/components/sessions/JKLobbyUserSwiper.js
@@ -1,6 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import JKProfileAvatar from '../profile/JKProfileAvatar';
+import { isIterableArray } from '../../helpers/utils';
+import Loader from '../common/Loader';
// import Swiper core and required modules
import SwiperCore, { Navigation, Pagination, Scrollbar, A11y } from 'swiper';
@@ -23,53 +25,64 @@ SwiperCore.use([Navigation, Pagination, Scrollbar, A11y]);
const JKLobbyUserSwiper = ({ onlineMusicians, setSelectedUsers, loadingStatus }) => {
return (
<>
-
console.log('slide change')}
- onSlideNextTransitionEnd={swiper => {
- if(swiper.isEnd){
- //goNextPage()
- }
- }}
- pagination={{
- clickable: true,
- type: 'custom'
- }}
- navigation={{
- nextEl: '.swiper-button-next',
- prevEl: '.swiper-button-prev'
- }}
- >
- {onlineMusicians.map((musician, index) => (
-
-
-
-
-
-
-
- {musician.name}
-
-
-
-
-
-
- ))}
-
-
+ {loadingStatus === 'loading' && onlineMusicians.length === 0 ? (
+
+ ) : (
+ <>
+ {isIterableArray(onlineMusicians) ? (
+ <>
+
console.log('slide change')}
+ onSlideNextTransitionEnd={swiper => {
+ if (swiper.isEnd) {
+ //goNextPage()
+ }
+ }}
+ pagination={{
+ clickable: true,
+ type: 'custom'
+ }}
+ navigation={{
+ nextEl: '.swiper-button-next',
+ prevEl: '.swiper-button-prev'
+ }}
+ >
+ {onlineMusicians.map((musician, index) => (
+
+
+
+
+
+
+ {musician.name}
+
+
+
+
+
+
+ ))}
+
+
+ >
+ ) : (
+
No musicians are currently available here.
+ )}
+ >
+ )}
>
);
};
JKLobbyUserSwiper.propTypes = {
onlineMusicians: PropTypes.arrayOf(PropTypes.instanceOf(Object)).isRequired,
- setSelectedUsers: PropTypes.func.isRequired,
+ setSelectedUsers: PropTypes.func.isRequired
};
export default JKLobbyUserSwiper;
diff --git a/jam-ui/src/context/BrowserQuery.js b/jam-ui/src/context/BrowserQuery.js
index bb9a6a2fd..d22d57a5a 100644
--- a/jam-ui/src/context/BrowserQuery.js
+++ b/jam-ui/src/context/BrowserQuery.js
@@ -4,7 +4,7 @@ import { useLocation } from "react-router-dom";
const BrowserQueryContext = React.createContext(null)
export const BrowserQueryProvider = ({children}) => {
-
+
function useQuery() {
return new URLSearchParams(useLocation().search);
}
diff --git a/jam-ui/src/helpers/rest.js b/jam-ui/src/helpers/rest.js
index 409be9f27..d932be8b1 100644
--- a/jam-ui/src/helpers/rest.js
+++ b/jam-ui/src/helpers/rest.js
@@ -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 = () => {
return new Promise((resolve, reject) => {
apiFetch('/genres')
diff --git a/jam-ui/src/layouts/JKDashboardLayout.js b/jam-ui/src/layouts/JKDashboardLayout.js
index 566733e44..94bb2b65c 100644
--- a/jam-ui/src/layouts/JKDashboardLayout.js
+++ b/jam-ui/src/layouts/JKDashboardLayout.js
@@ -7,6 +7,8 @@ import { BrowserQueryProvider } from '../context/BrowserQuery';
import { NativeAppProvider } from '../context/NativeAppContext';
+import { JKLobbyChatProvider } from '../components/sessions/JKLobbyChatContext';
+
const DashboardLayout = ({ location }) => {
useEffect(() => {
window.scrollTo(0, 0);
@@ -16,7 +18,9 @@ const DashboardLayout = ({ location }) => {
-
+
+
+
diff --git a/jam-ui/src/store/features/lobbyChatMessagesSlice.js b/jam-ui/src/store/features/lobbyChatMessagesSlice.js
index e41a23547..7361cfc24 100644
--- a/jam-ui/src/store/features/lobbyChatMessagesSlice.js
+++ b/jam-ui/src/store/features/lobbyChatMessagesSlice.js
@@ -43,6 +43,18 @@ const chatMessagesSlice = createSlice({
};
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) => {
state.error = action.payload;
state.status = 'failed';
diff --git a/jam-ui/src/store/features/onlineMusiciansSlice.js b/jam-ui/src/store/features/onlineMusiciansSlice.js
index 56412d1f3..b7647ae3a 100644
--- a/jam-ui/src/store/features/onlineMusiciansSlice.js
+++ b/jam-ui/src/store/features/onlineMusiciansSlice.js
@@ -1,5 +1,5 @@
import {createSlice, createAsyncThunk} from "@reduxjs/toolkit";
-import { getPeopleIndex } from "../../helpers/rest";
+import { getPeopleIndex, getLobbyUsers } from "../../helpers/rest";
const initialState = {
musicians: [],
@@ -10,7 +10,8 @@ const initialState = {
export const fetchOnlineMusicians = createAsyncThunk(
'onlineMusician/fetchMusicians',
async (options, thunkAPI) => {
- const response = await getPeopleIndex(options)
+ //const response = await getPeopleIndex(options)
+ const response = await getLobbyUsers(options)
return response.json()
}
)
@@ -32,6 +33,7 @@ export const onlineMusiciansSlice = createSlice({
state.status = 'loading'
})
.addCase(fetchOnlineMusicians.fulfilled, (state, action) => {
+ console.log('fetchOnlineMusicians.fulfilled', action.payload)
state.status = 'succeeded'
state.musicians = action.payload
})
diff --git a/ruby/db/migrate/20240121174150_add_accept_desktop_notifications_to_users.rb b/ruby/db/migrate/20240121174150_add_accept_desktop_notifications_to_users.rb
new file mode 100644
index 000000000..750dec0e4
--- /dev/null
+++ b/ruby/db/migrate/20240121174150_add_accept_desktop_notifications_to_users.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/models/chat_message.rb b/ruby/lib/jam_ruby/models/chat_message.rb
index d196c1785..618b3d372 100644
--- a/ruby/lib/jam_ruby/models/chat_message.rb
+++ b/ruby/lib/jam_ruby/models/chat_message.rb
@@ -187,7 +187,7 @@ module JamRuby
elsif channel == 'global'
@@mq_router.publish_to_active_clients(msg)
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'
@@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
diff --git a/ruby/lib/jam_ruby/models/text_message.rb b/ruby/lib/jam_ruby/models/text_message.rb
index 09e3f7486..6b4096eea 100644
--- a/ruby/lib/jam_ruby/models/text_message.rb
+++ b/ruby/lib/jam_ruby/models/text_message.rb
@@ -52,6 +52,8 @@ module JamRuby
tm.message = sanitized_text
tm.target_user_id = target_user_id
tm.source_user_id = source_user_id
+ tm.valid?
+ Rails.logger.info "___ERROR #{tm.errors.inspect}" unless tm.errors.empty?
tm.save
# send notification
diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb
index d076f2e98..2b67ea537 100644
--- a/ruby/lib/jam_ruby/models/user.rb
+++ b/ruby/lib/jam_ruby/models/user.rb
@@ -21,6 +21,7 @@ module JamRuby
JAM_REASON_JOIN = 'j'
JAM_REASON_IMPORT = 'i'
JAM_REASON_LOGIN = 'l'
+ JAM_REASON_PRESENT = 'p'
# MOD KEYS
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 :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
if school_interest && !school_interest_was
if education_interest
@@ -1959,21 +1968,28 @@ module JamRuby
def update_addr_loc(connection, reason)
unless connection
@@log.warn("no connection specified in update_addr_loc with reason #{reason}")
+ update_last_active(reason)
return
end
if connection.locidispid.nil?
@@log.warn("no locidispid for connection's ip_address: #{connection.ip_address}")
+ update_last_active(reason)
return
end
# 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
+ update_last_active(reason)
return
end
self.last_jam_addr = connection.addr
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_at = Time.now
unless self.save
diff --git a/web/app/assets/javascripts/modern/JamServer_copy.js b/web/app/assets/javascripts/modern/JamServer_copy.js
index 8cde65fe9..60bdffef5 100644
--- a/web/app/assets/javascripts/modern/JamServer_copy.js
+++ b/web/app/assets/javascripts/modern/JamServer_copy.js
@@ -16,6 +16,7 @@
context.WEB_SOCKET_SWF_LOCATION = "assets/flash/WebSocketMain.swf";
context.JK.JamServer = function (app, activeElementEvent) {
+ console.log("_DEBUG Init JK scripts....", context.JK.JamServer);
// uniquely identify the websocket connection
var channelId = null;
var clientType = null;
@@ -197,11 +198,11 @@
function loggedIn(header, payload) {
-
+ console.log("___loggedIn", header, payload)
// reason for setTimeout:
// 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...
-
+
setTimeout(function() {
server.signedIn = true;
server.clientID = payload.client_id;
@@ -217,7 +218,7 @@
heartbeatStateReset();
app.clientId = payload.client_id;
-
+
if (isClientMode() && context.jamClient) {
// tell the backend that we have logged in
try {
@@ -247,6 +248,7 @@
// 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*
// it to go through unless we've passed through .OnLoggedIn
+
server.connected = true;
server.reconnecting = false;
server.connecting = false;
@@ -315,17 +317,18 @@
}
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;
setActive(active)
activityTimeout = setTimeout(markAway, timeoutTime);
$(document).ready(function() {
$('body').bind('mousedown keydown touchstart focus', function(event) {
+
if (activityTimeout) {
clearTimeout(activityTimeout);
activityTimeout = null;
}
-
if (!active) {
if(server && server.connected) {
logger.debug("awake again!")
@@ -701,6 +704,7 @@
server.socket.onopen = server.onOpen;
server.socket.onmessage = server.onMessage;
server.socket.onclose = server.onClose;
+ server.socket.onerror = server.onError;
connectTimeout = setTimeout(function () {
logger.debug("connection timeout fired")
@@ -710,7 +714,7 @@
server.close(true);
connectDeferred.reject();
}
- }, 4000);
+ }, 10000);
return connectDeferred;
};
@@ -740,15 +744,16 @@
server.onOpen = function () {
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) {
+ console.log("server.onMessage", e)
var message = JSON.parse(e.data),
messageType = message.type.toLowerCase(),
payload = message[messageType],
callbacks = server.dispatchTable[message.type];
-
+ console.log("server.onMessage", message, messageType, payload, callbacks)
if (message.type == context.JK.MessageType.PEER_MESSAGE) {
console.log("server.onMessage:" + messageType);
}
@@ -773,8 +778,8 @@
};
// onClose is called if either client or server closes connection
- server.onClose = function () {
- logger.info("Socket to server closed.");
+ server.onClose = function (e) {
+ logger.info("Socket to server closed.", e.code, e.reason);
var disconnectedSocket = this;
@@ -790,6 +795,10 @@
closedCleanup(true);
};
+ server.onError = function (e) {
+ logger.error("Socket error.", e);
+ };
+
server.send = function (message) {
var jsMessage = JSON.stringify(message);
@@ -942,9 +951,7 @@
registerServerRejection();
registerSocketClosed();
activityCheck();
-
-
- console.log("Init JK scripts....", context.JK.JamServer);
+
context.JK.JamServer.send("hello")
// $inSituBanner = $('.server-connection');
diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb
index 4e2eb02b2..5173d7ff6 100644
--- a/web/app/controllers/api_users_controller.rb
+++ b/web/app/controllers/api_users_controller.rb
@@ -25,6 +25,11 @@ class ApiUsersController < ApiController
@users = User.paginate(page: params[:page])
respond_with @users, responder: ApiResponder, :status => 200
end
+
+ def lobby
+ @users = User.lobby(current_user)
+ respond_with @users, responder: ApiResponder, :status => 200
+ end
def calendar
#@user=lookup_user
@@ -230,6 +235,7 @@ class ApiUsersController < ApiController
@user.education_interest = !!params[:education_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.accept_desktop_notifications = params[:accept_desktop_notifications] if params.has_key?(:accept_desktop_notifications)
if @user.save
test_drive_package_details = params[:test_drive_package]
diff --git a/web/app/views/api_users/lobby.rabl b/web/app/views/api_users/lobby.rabl
new file mode 100644
index 000000000..6b7765693
--- /dev/null
+++ b/web/app/views/api_users/lobby.rabl
@@ -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
\ No newline at end of file
diff --git a/web/config/routes.rb b/web/config/routes.rb
index aa85799ef..d228494d3 100644
--- a/web/config/routes.rb
+++ b/web/config/routes.rb
@@ -391,6 +391,7 @@ Rails.application.routes.draw do
match '/me' => 'api_users#me', :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/:id' => 'api_users#show', :via => :get, :as => 'api_user_detail'
match '/users/:id/authorizations' => 'api_users#authorizations', :via => :get
diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb
index fbb2b70dd..eabf652d0 100644
--- a/websocket-gateway/lib/jam_websockets/router.rb
+++ b/websocket-gateway/lib/jam_websockets/router.rb
@@ -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
@background_thread = Thread.new {
- count = 0
+ client_check_count = 0
+ user_last_seen_count = 0
while true
@@ -103,11 +104,17 @@ module JamWebsockets
periodical_check_connections
periodical_notification_seen
- if count == 30
+ if client_check_count == 30
periodical_check_clients
- count = 0
+ client_check_count = 0
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
@log.error "unhandled error in thread #{e}"
@@ -1514,6 +1521,21 @@ module JamWebsockets
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
# assume 60 seconds per status dump