Merge branch 'develop' into seth/obs-download

This commit is contained in:
Seth Call 2025-06-14 19:39:23 -05:00
commit a64873d7a6
40 changed files with 908 additions and 92 deletions

View File

@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { fetchSessions } from '../../store/features/sessionsSlice';
import { fetchSessions, fetchFriendsSessions, fetchPublicSessions, fetchInactiveSessions } from '../../store/features/sessionsSlice';
import { isIterableArray } from '../../helpers/utils';
import { useResponsive } from '@farfetch/react-context-responsive';
import JKModalDialog from '../common/JKModalDialog';
@ -23,7 +23,10 @@ function JKMusicSessions() {
const { nativeAppUnavailable, setNativeAppUnavailable } = useNativeApp();
useEffect(() => {
dispatch(fetchSessions());
// dispatch(fetchSessions());
dispatch(fetchFriendsSessions());
dispatch(fetchPublicSessions());
//dispatch(fetchInactiveSessions());
}, []);
const toggleAppUnavilableModel = () => {
@ -37,7 +40,7 @@ function JKMusicSessions() {
<CardBody className="pt-0">
{loadingStatus === 'loading' && sessions.length === 0 ? (
<Loader />
) : isIterableArray(sessions) ? (
) : loadingStatus === "succeeded" && sessions.length > 0 ? (
<>
{greaterThan.sm ? (
<Row className="mb-3 justify-content-between d-none d-md-block">
@ -51,7 +54,7 @@ function JKMusicSessions() {
</Row>
)}
</>
) : (
) : loadingStatus === "succeeded" && sessions.length <= 0 ? (
<Row className="p-card">
<Col>
{t('list.no_records_1', { ns: 'sessions' })}
@ -59,7 +62,13 @@ function JKMusicSessions() {
{t('list.no_records_2', { ns: 'sessions' })}
</Col>
</Row>
)}
) : loadingStatus === "failed" ? (
<Row className="p-card">
<Col>
{t('list.error', { ns: 'sessions' })}
</Col>
</Row>
) : null }
</CardBody>
</Card>
<JKModalDialog

View File

@ -122,7 +122,7 @@ const JKDownloads = () => {
</p>
<div className='mt-2 d-flex flex-column flex-md-row'>
<div style={ { flexGrow: 0, flexShrink: 0, flexBasis: '25%' } }>
<a href={downloadLink} target='_blank'>
<a href={downloadLink} target='_blank' style={{ cursor: 'pointer' }}>
<img src={downloadImageUrl} alt="Download JamKazam" />
</a>
</div>

View File

@ -72,8 +72,9 @@ const JKDownloadsLegacy = () => {
const downloadLink = React.useMemo(() => {
if (!currentOS) return null
return downloads[`JamClient/${currentOS}`]
}, [currentOS]);
if (!selectedPlatform) return null
return downloads[`JamClient/${selectedPlatform}`]
}, [currentOS, selectedPlatform]);
const selectPlatform = (platform) => {
setSelectedPlatform(platform)
@ -126,7 +127,7 @@ const JKDownloadsLegacy = () => {
</p>
<div className='mt-2 d-flex flex-column flex-md-row'>
<div style={ { flexGrow: 0, flexShrink: 0, flexBasis: '25%' } }>
<a href={downloadLink} target='_blank'>
<a href={downloadLink} target='_blank' style={ { cursor: 'pointer' } }>
<img src={downloadImageUrl} alt="Download JamKazam" />
</a>
</div>

View File

@ -28,7 +28,7 @@ function JKSession({ session }) {
useEffect(() => {
const otherUserIds = session.participants.map(p => p.user.id);
const otherUserIds = session.active_music_session ? session.active_music_session.participants.map(p => p.user.id) : [];
const options = { currentUserId: currentUser.id, otherUserIds };
dispatch(fetchUserLatencies(options));
}, [session.id]);
@ -40,7 +40,7 @@ function JKSession({ session }) {
};
const hasFriendNote = session => {
if (session.participants.find(p => p.user.is_friend)) {
if (session.active_music_session && session.active_music_session.participants.find(p => p.user.is_friend)) {
return t('list.notes.has_friend', { ns: 'sessions' });
}
};
@ -71,26 +71,38 @@ function JKSession({ session }) {
</div>
<div>{sessionDescription}</div>
</td>
<td>
{session.participants.map(participant => (
{session.active_music_session && session.active_music_session.participants.length > 0 && (
<>
{session.active_music_session.participants.map(participant => (
<Row style={musicianRowStyle} key={participant.id}>
<Col>
<JKSessionUser user={participant.user} />
</Col>
</Row>
))}
</>
)}
</td>
<td className="text-center">
{session.participants.map(participant => (
{session.active_music_session && session.active_music_session.participants.length > 0 && (
<>
{session.active_music_session.participants.map(participant => (
<Row key={participant.id} style={musicianRowStyle}>
<Col>
<JKUserLatencyBadge key={participant.id} user={participant.user} showBadgeOnly={true} />
</Col>
</Row>
))}
</>
)}
</td>
<td>
{session.participants.map(participant => (
{session.active_music_session && session.active_music_session.participants.length > 0 && (
<>
{session.active_music_session.participants.map(participant => (
<Row style={musicianRowStyle} key={participant.id} data-testid={`Participant${participant.id}Tracks`}>
<Col>
{participant.tracks.map(track => (
@ -113,6 +125,8 @@ function JKSession({ session }) {
</Col>
</Row>
))}
</>
)}
</td>
<td className="text-center">
<JoinSessionButton session={session} />
@ -142,11 +156,13 @@ function JKSession({ session }) {
<strong>{t('list.header.latency', { ns: 'sessions' })}</strong>
</div>
</div>
{session.active_music_session && session.active_music_session.participants.length > 0 && (
<div>
{session.participants.map(participant => (
{session.active_music_session.participants.map(participant => (
<JKSessionUser key={participant.id} user={participant.user} />
))}
</div>
)}
<div className="mt-4 d-flex flex-row justify-content-center">
<JoinSessionButton session={session} />
</div>

View File

@ -213,6 +213,30 @@ export const getSessions = () => {
});
};
export const getFriendsSessions = () => {
return new Promise((resolve, reject) => {
apiFetch(`/sessions/friends`)
.then(response => resolve(response))
.catch(error => reject(error));
});
};
export const getPublicSessions = () => {
return new Promise((resolve, reject) => {
apiFetch(`/sessions/public`)
.then(response => resolve(response))
.catch(error => reject(error));
});
};
export const getInactiveSessions = () => {
return new Promise((resolve, reject) => {
apiFetch(`/sessions/inactive`)
.then(response => resolve(response))
.catch(error => reject(error));
});
};
export const getSessionsHistory = (options = {}) => {
return new Promise((resolve, reject) => {
apiFetch(`/sessions/history?${new URLSearchParams(options)}`)

View File

@ -40,7 +40,8 @@
},
"no_records_1": "There are no public, open sessions currently available for you to join. We suggest you ",
"create_session": "create a session",
"no_records_2": " that others can join now, or wait a bit and then refresh this page in your browser to see if new public sessions have been started."
"no_records_2": " that others can join now, or wait a bit and then refresh this page in your browser to see if new public sessions have been started.",
"error": "There was an error retrieving the session list. Please try again."
},
"lobby": {
"page_title": "Lobby",

View File

@ -1,5 +1,5 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { getSessions, getPersonById } from '../../helpers/rest'
import { getSessions, getFriendsSessions, getPublicSessions, getInactiveSessions, getPersonById } from '../../helpers/rest'
const initialState = {
sessions: [],
@ -14,7 +14,30 @@ export const fetchSessions = createAsyncThunk(
const response = await getSessions();
return response.json();
}
)
export const fetchFriendsSessions = createAsyncThunk(
"session/fetchFriendsSessions",
async (options, thunkAPI) => {
const response = await getFriendsSessions(options);
return response.json();
}
)
export const fetchPublicSessions = createAsyncThunk(
"session/fetchPublicSessions",
async (options, thunkAPI) => {
const response = await getPublicSessions(options);
return response.json();
}
)
export const fetchInactiveSessions = createAsyncThunk(
"session/fetchInactiveSessions",
async (options, thunkAPI) => {
const response = await getInactiveSessions(options);
return response.json();
}
)
export const fetchPerson = createAsyncThunk(
@ -51,14 +74,54 @@ export const SessionSlice = createSlice({
state.status = "loading";
})
.addCase(fetchSessions.fulfilled, (state, action) => {
console.log(action.payload);
// add unique sessions to the array
const records = new Set([...state.sessions, ...action.payload]);
const unique = [];
records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x))
state.sessions = unique;
state.error = null;
state.status = "succeeded";
state.sessions = action.payload;
})
.addCase(fetchSessions.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
.addCase(fetchFriendsSessions.pending, (state, action) => {
state.status = "loading";
})
.addCase(fetchFriendsSessions.fulfilled, (state, action) => {
// add unique sessions to the array
const records = new Set([...state.sessions, ...action.payload.sessions]);
const unique = [];
records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x))
state.sessions = unique;
state.error = null;
state.status = "succeeded";
})
.addCase(fetchPublicSessions.pending, (state, action) => {
state.status = "loading";
})
.addCase(fetchPublicSessions.fulfilled, (state, action) => {
// add unique sessions to the array
const records = new Set([...state.sessions, ...action.payload.sessions]);
const unique = [];
records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x))
state.sessions = unique;
state.error = null;
state.status = "succeeded";
})
.addCase(fetchInactiveSessions.pending, (state, action) => {
state.status = "loading";
})
.addCase(fetchInactiveSessions.fulfilled, (state, action) => {
// add unique sessions to the array
const records = new Set([...state.sessions, ...action.payload.sessions]);
const unique = [];
records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x))
state.sessions = unique;
state.error = null;
state.status = "succeeded";
})
.addCase(fetchPerson.fulfilled, (state, action) => {
const person = state.people.find(person => person.id === action.payload.id)
if (person) {

View File

@ -0,0 +1,29 @@
class AddProfileCompleteColumnsToUsers < ActiveRecord::Migration
def self.up
execute "ALTER TABLE users ADD COLUMN profile_completed_at TIMESTAMP"
#add index on profile_completed_at
execute "CREATE INDEX index_users_on_profile_completed_at ON users USING btree (profile_completed_at)"
execute "ALTER TABLE users ADD COLUMN profile_complete_reminder1_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN profile_complete_reminder2_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN profile_complete_reminder3_sent_at TIMESTAMP"
User.find_each(batch_size:100) do |user|
User.where(id:user.id).update_all(profile_completed_at: Time.now)
end
#User.where('users.id IN (SELECT player_id FROM musicians_instruments) OR users.id IN (SELECT player_id FROM genre_players)').update_all(profile_completed_at: Time.now)
end
def self.down
execute "ALTER TABLE users DROP COLUMN profile_completed_at"
execute "ALTER TABLE users DROP COLUMN profile_complete_reminder1_sent_at"
execute "ALTER TABLE users DROP COLUMN profile_complete_reminder2_sent_at"
execute "ALTER TABLE users DROP COLUMN profile_complete_reminder3_sent_at"
end
end
=begin
ALTER TABLE users ADD COLUMN profile_completed_at TIMESTAMP;
CREATE INDEX index_users_on_profile_completed_at ON users USING btree (profile_completed_at);
ALTER TABLE users ADD COLUMN profile_complete_reminder1_sent_at TIMESTAMP;
ALTER TABLE users ADD COLUMN profile_complete_reminder2_sent_at TIMESTAMP;
ALTER TABLE users ADD COLUMN profile_complete_reminder3_sent_at TIMESTAMP;
UPDATE users set profile_completed_at=NOW() WHERE users.id IN (SELECT player_id FROM musicians_instruments) OR users.id IN (SELECT player_id FROM genre_players);
end

View File

@ -0,0 +1,20 @@
class AddGearSetupReminderColumnsToUsers < ActiveRecord::Migration
def self.up
execute "ALTER TABLE users ADD COLUMN gear_setup_reminder1_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN gear_setup_reminder2_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN gear_setup_reminder3_sent_at TIMESTAMP"
end
def self.down
execute "ALTER TABLE users DROP COLUMN gear_setup_reminder1_sent_at"
execute "ALTER TABLE users DROP COLUMN gear_setup_reminder2_sent_at"
execute "ALTER TABLE users DROP COLUMN gear_setup_reminder3_sent_at"
end
end
=begin
ALTER TABLE users ADD COLUMN gear_setup_reminder1_sent_at TIMESTAMP;
ALTER TABLE users ADD COLUMN gear_setup_reminder2_sent_at TIMESTAMP;
ALTER TABLE users ADD COLUMN gear_setup_reminder3_sent_at TIMESTAMP;
end

View File

@ -0,0 +1,19 @@
class AddSignupSurveySentAtToUsers < ActiveRecord::Migration
def self.up
execute "ALTER TABLE users ADD COLUMN signup_survey_sent_at TIMESTAMP"
User.find_each(batch_size:100) do |user|
User.where(id:user.id).update_all(signup_survey_sent_at: Time.now)
end
end
def self.down
execute "ALTER TABLE users DROP COLUMN signup_survey_sent_at"
end
end
=begin
ALTER TABLE users ADD COLUMN signup_survey_sent_at TIMESTAMP;
CREATE INDEX index_users_on_signup_survey_sent_at ON users USING btree (signup_survey_sent_at);
User.find_each(batch_size:100) do |user|
User.where(id:user.id).update_all(signup_survey_sent_at: Time.now)
end
=end

View File

@ -120,6 +120,9 @@ require "jam_ruby/lib/subscription_message"
require "jam_ruby/lib/stats.rb"
require "jam_ruby/lib/email_new_musician_match"
require "jam_ruby/lib/musician_filter"
require "jam_ruby/lib/email_profile_reminder"
require "jam_ruby/lib/email_signup_survey"
require "jam_ruby/lib/gear_setup_reminder"
require "jam_ruby/amqp/amqp_connection_manager"
require "jam_ruby/database"
require "jam_ruby/message_factory"

View File

@ -418,6 +418,78 @@ module JamRuby
end
end
def profile_complete_reminder1(user)
@user = user
sendgrid_recipients([user.email])
sendgrid_substitute('@USERID', [user.id])
sendgrid_unique_args :type => "profile_complete_reminder1"
mail(:to => user.email, :subject => I18n.t('user_mailer.profile_complete_reminder1.subject')) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
end
def profile_complete_reminder2(user)
@user = user
sendgrid_recipients([user.email])
sendgrid_substitute('@USERID', [user.id])
sendgrid_unique_args :type => "profile_complete_reminder2"
mail(:to => user.email, :subject => "Take 2 minutes to fill out your JamKazam profile now") do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
end
def profile_complete_reminder3(user)
@user = user
sendgrid_recipients([user.email])
sendgrid_substitute('@USERID', [user.id])
sendgrid_unique_args :type => "profile_complete_reminder3"
mail(:to => user.email, :subject => "Last reminder to update your JamKazam profile") do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
end
#################################### GEAR SETUP REMINDER EMAILS ####################################
def gear_setup_reminder1(user)
@user = user
mail(:to => user.email, :subject => I18n.t('user_mailer.gear_setup_reminder1.subject')) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
end
def gear_setup_reminder2(user)
@user = user
mail(:to => user.email, :subject => I18n.t('user_mailer.gear_setup_reminder2.subject')) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
end
def gear_setup_reminder3(user)
@user = user
mail(:to => user.email, :subject => I18n.t('user_mailer.gear_setup_reminder3.subject')) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
end
def signup_survey(user)
@user = user
@subject = I18n.t('user_mailer.signup_survey.subject')
@survey_url = Rails.application.config.signup_survey_url
mail(:to => user.email, :subject => @subject) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
end
#################################### NOTIFICATION EMAILS ####################################
def friend_request(user, msg, friend_request_id)
return if !user.subscribe_email

View File

@ -0,0 +1,22 @@
<p><%=I18n.t('user_mailer.gear_setup_reminder1.greeting') -%> <%= @user.first_name -%> -</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder1.paragraph1').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder1.paragraph2').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder1.paragraph3').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder1.paragraph4').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder1.regards') -%>,<br />
<%=I18n.t('user_mailer.gear_setup_reminder1.signature') -%>
</p>

View File

@ -0,0 +1,12 @@
<%=I18n.t('user_mailer.gear_setup_reminder1.greeting') -%> <%= @user.first_name -%>
<%=I18n.t('user_mailer.gear_setup_reminder1.paragraph1') -%>
<%=I18n.t('user_mailer.gear_setup_reminder1.paragraph2') -%>
<%=I18n.t('user_mailer.gear_setup_reminder1.paragraph3') -%>
<%=I18n.t('user_mailer.gear_setup_reminder1.paragraph4') -%>
<%=I18n.t('user_mailer.gear_setup_reminder1.regards') -%>,
<%=I18n.t('user_mailer.gear_setup_reminder1.signature') -%>

View File

@ -0,0 +1,26 @@
<p><%=I18n.t('user_mailer.gear_setup_reminder2.greeting') -%> <%= @user.first_name -%> -</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder2.paragraph1').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder2.paragraph2').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder2.paragraph3').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder2.paragraph4').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder2.paragraph5').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder2.regards') -%>,<br />
<%=I18n.t('user_mailer.gear_setup_reminder2.signature') -%>
</p>

View File

@ -0,0 +1,14 @@
<%=I18n.t('user_mailer.gear_setup_reminder2.greeting') -%> <%= @user.first_name -%>
<%=I18n.t('user_mailer.gear_setup_reminder2.paragraph1') -%>
<%=I18n.t('user_mailer.gear_setup_reminder2.paragraph2') -%>
<%=I18n.t('user_mailer.gear_setup_reminder2.paragraph3') -%>
<%=I18n.t('user_mailer.gear_setup_reminder2.paragraph4') -%>
<%=I18n.t('user_mailer.gear_setup_reminder2.paragraph5') -%>
<%=I18n.t('user_mailer.gear_setup_reminder2.regards') -%>,
<%=I18n.t('user_mailer.gear_setup_reminder2.signature') -%>

View File

@ -0,0 +1,26 @@
<p><%=I18n.t('user_mailer.gear_setup_reminder3.greeting') -%> <%= @user.first_name -%> -</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder3.paragraph1').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder3.paragraph2').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder3.paragraph3').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder3.paragraph4').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder3.paragraph5').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.gear_setup_reminder3.regards') -%>,<br />
<%=I18n.t('user_mailer.gear_setup_reminder3.signature') -%>
</p>

View File

@ -0,0 +1,14 @@
<%=I18n.t('user_mailer.gear_setup_reminder3.greeting') -%> <%= @user.first_name -%>
<%=I18n.t('user_mailer.gear_setup_reminder3.paragraph1') -%>
<%=I18n.t('user_mailer.gear_setup_reminder3.paragraph2') -%>
<%=I18n.t('user_mailer.gear_setup_reminder3.paragraph3') -%>
<%=I18n.t('user_mailer.gear_setup_reminder3.paragraph4') -%>
<%=I18n.t('user_mailer.gear_setup_reminder3.paragraph5') -%>
<%=I18n.t('user_mailer.gear_setup_reminder3.regards') -%>,
<%=I18n.t('user_mailer.gear_setup_reminder3.signature') -%>

View File

@ -0,0 +1,73 @@
<p><%=I18n.t('user_mailer.profile_complete_reminder1.greeting') -%> <%= @user.first_name -%> -</p>
<p>
<%=I18n.t('user_mailer.profile_complete_reminder1.paragraph1') -%>
</p>
<p style="text-align:center">
<a href="<%= APP_CONFIG.spa_origin_url %>/profile" style="color: #fff;
text-decoration: none;
background-color: #2c7be5;
border-color: #2c7be5;
border: 1px solid transparent;
padding: 0.3125rem 1rem;
line-height: 2.5;
font-size: 1em;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out,
background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
box-shadow 0.15s ease-in-out;">
<%=I18n.t('user_mailer.profile_complete_reminder1.update_profile') -%>
</a>
</p>
<p>
<%=I18n.t('user_mailer.profile_complete_reminder1.paragraph2') -%>
</p>
<p>
<%=I18n.t('user_mailer.profile_complete_reminder1.paragraph3') -%>
</p>
<p style="text-align:center">
<a href="<%= APP_CONFIG.external_root_url %>/downloads" style="color: #fff;
text-decoration: none;
background-color: #2c7be5;
border-color: #2c7be5;
border: 1px solid transparent;
padding: 0.3125rem 1rem;
margin-right: 1rem;
line-height: 2.5;
font-size: 1em;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out,
background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
box-shadow 0.15s ease-in-out;">
<%=I18n.t('user_mailer.profile_complete_reminder1.download_app') -%>
</a>
<a href="<%= APP_CONFIG.external_root_url %>/downloads" style="color: #fff;
text-decoration: none;
background-color: #2c7be5;
border-color: #2c7be5;
border: 1px solid transparent;
padding: 0.3125rem 1rem;
line-height: 2.5;
font-size: 1em;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out,
background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
box-shadow 0.15s ease-in-out;">
<%=I18n.t('user_mailer.profile_complete_reminder1.download_legacy_app') -%>
</a>
</p>
<p>
<%=I18n.t('user_mailer.profile_complete_reminder1.paragraph4') -%>
</p>
<div>
<p>
<%=I18n.t('user_mailer.profile_complete_reminder1.regards') -%>
<br />
<%=I18n.t('user_mailer.profile_complete_reminder1.signature') -%>
</p>
</div>

View File

@ -0,0 +1,17 @@
<%=I18n.t('user_mailer.profile_complete_reminder1.greeting') -%> <%= @user.first_name -%> -
<%=I18n.t('user_mailer.profile_complete_reminder1.paragraph1') -%>
<%= APP_CONFIG.spa_origin_url %>/profile
<%=I18n.t('user_mailer.profile_complete_reminder1.paragraph2') -%>
<%=I18n.t('user_mailer.profile_complete_reminder1.paragraph3') -%>
<%= APP_CONFIG.external_root_url %>/downloads
<%=I18n.t('user_mailer.profile_complete_reminder1.paragraph4') -%>
<%=I18n.t('user_mailer.profile_complete_reminder1.regards') -%>
<%=I18n.t('user_mailer.profile_complete_reminder1.signature') -%>

View File

@ -0,0 +1,34 @@
<p><%=I18n.t('user_mailer.profile_complete_reminder2.greeting') -%>&nbsp;<%= @user.first_name -%> -</p>
<p>
<%=I18n.t('user_mailer.profile_complete_reminder2.paragraph1') -%>
</p>
<p style="text-align:center">
<a href="<%= APP_CONFIG.spa_origin_url %>/profile" style="color: #fff;
text-decoration: none;
background-color: #2c7be5;
border-color: #2c7be5;
border: 1px solid transparent;
padding: 0.3125rem 1rem;
line-height: 2.5;
font-size: 1em;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out,
background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
box-shadow 0.15s ease-in-out;">
<%=I18n.t('user_mailer.profile_complete_reminder2.update_profile') -%>
</a>
</p>
<p>
<%=I18n.t('user_mailer.profile_complete_reminder2.paragraph2') -%>
</p>
<div>
<p>
<%=I18n.t('user_mailer.profile_complete_reminder2.regards') -%>
<br />
<%=I18n.t('user_mailer.profile_complete_reminder2.signature') -%>
</p>
</div>

View File

@ -0,0 +1,11 @@
<%=I18n.t('user_mailer.profile_complete_reminder2.greeting') -%> <%= @user.first_name -%> -
<%=I18n.t('user_mailer.profile_complete_reminder2.paragraph1') -%>
<%= APP_CONFIG.spa_origin_url %>/profile
<%=I18n.t('user_mailer.profile_complete_reminder2.paragraph2') -%>
<%=I18n.t('user_mailer.profile_complete_reminder2.regards') -%>
<%=I18n.t('user_mailer.profile_complete_reminder2.signature') -%>

View File

@ -0,0 +1,29 @@
<p><%=I18n.t('user_mailer.profile_complete_reminder3.greeting') -%>&nbsp;<%= @user.first_name -%> -</p>
<p><%=I18n.t('user_mailer.profile_complete_reminder3.paragraph1') -%>
</p>
<p style="text-align:center">
<a href="<%= APP_CONFIG.spa_origin_url %>/profile" style="color: #fff;
text-decoration: none;
background-color: #2c7be5;
border-color: #2c7be5;
border: 1px solid transparent;
padding: 0.3125rem 1rem;
line-height: 2.5;
font-size: 1em;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out,
background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
box-shadow 0.15s ease-in-out;">
<%=I18n.t('user_mailer.profile_complete_reminder3.update_profile') -%>
</a>
</p>
<div>
<p>
<%=I18n.t('user_mailer.profile_complete_reminder3.regards') -%>
<br />
<%=I18n.t('user_mailer.profile_complete_reminder3.signature') -%>
</p>
</div>

View File

@ -0,0 +1,9 @@
<%=I18n.t('user_mailer.profile_complete_reminder3.greeting') -%><%= @user.first_name -%> -
<%=I18n.t('user_mailer.profile_complete_reminder3.paragraph1') -%>
<%= APP_CONFIG.spa_origin_url %>/profile
<%=I18n.t('user_mailer.profile_complete_reminder3.regards') -%>
<%=I18n.t('user_mailer.profile_complete_reminder3.signature') -%>

View File

@ -0,0 +1,18 @@
<p><%= I18n.t 'user_mailer.signup_survey.greeting' -%> <%= @user.first_name -%> -</p>
<p>
<%= I18n.t 'user_mailer.signup_survey.paragraph1' -%>
</p>
<p>
<%= @survey_url %>
</p>
<p>
<%= I18n.t 'user_mailer.signup_survey.ps' -%>
<%= I18n.t 'user_mailer.signup_survey.ps_text' -%>
</p>
<p><%= I18n.t 'user_mailer.signup_survey.regards' -%><br/>
<%= I18n.t 'user_mailer.signup_survey.signature' -%>
</p>

View File

@ -0,0 +1,11 @@
<%= I18n.t 'user_mailer.signup_survey.greeting' -%> <%= @user.first_name -%> -
<%= I18n.t 'user_mailer.signup_survey.paragraph1' -%>
<%= @survey_url %>
<%= I18n.t 'user_mailer.signup_survey.ps' -%>
<%= I18n.t 'user_mailer.signup_survey.ps_text' -%>
<%= I18n.t 'user_mailer.signup_survey.regards' -%>,
<%= I18n.t 'user_mailer.signup_survey.signature' -%>

View File

@ -0,0 +1,48 @@
module JamRuby
class EmailProfileReminder
@@log = Logging.logger[EmailProfileReminder]
def self.send_reminders
begin
#If the user has not updated their profile 1 day after signup, then send reminder email1
reminder1_users.find_each do |user|
UserMailer.profile_complete_reminder1(user).deliver_now
User.where(id: user.id).update_all(profile_complete_reminder1_sent_at: Time.now)
end
#If the user has not updated their profile 3 days after signup, then send reminder email2
reminder2_users.find_each do |user|
UserMailer.profile_complete_reminder2(user).deliver_now
User.where(id: user.id).update_all(profile_complete_reminder2_sent_at: Time.now)
end
#If the user has not updated their profile 5 days after signup, then send reminder email3
reminder3_users.find_each do |user|
UserMailer.profile_complete_reminder3(user).deliver_now
User.where(id: user.id).update_all(profile_complete_reminder3_sent_at: Time.now)
end
rescue Exception => e
@@log.error("unable to send profile reminder email=#{e}")
puts "unable to send profile reminder email=#{e}"
end
end
def self.prospect_users
User.where("users.profile_completed_at IS NULL AND users.subscribe_email = ?", true)
end
def self.reminder1_users
EmailProfileReminder.prospect_users.where("users.created_at < ? AND users.profile_complete_reminder1_sent_at IS NULL", 1.day.ago)
end
def self.reminder2_users
EmailProfileReminder.prospect_users.where("users.created_at < ? AND users.profile_complete_reminder1_sent_at IS NOT NULL AND users.profile_complete_reminder2_sent_at IS NULL", 3.days.ago)
end
def self.reminder3_users
EmailProfileReminder.prospect_users.where("users.created_at < ? AND users.profile_complete_reminder2_sent_at IS NOT NULL AND users.profile_complete_reminder3_sent_at IS NULL", 5.days.ago)
end
end
end

View File

@ -0,0 +1,23 @@
module JamRuby
class EmailSignupSurvey
@@log = Logging.logger[EmailSignupSurvey]
def self.send_survey
begin
# if signup survey email has not been sent to this user, then send it
survey_users.find_each do |user|
UserMailer.signup_survey(user).deliver_now
User.where(id: user.id).update_all(signup_survey_sent_at: Time.now)
end
rescue Exception => e
@@log.error("unable to send surevy email e=#{e}")
puts "unable to send surevy email e=#{e}"
end
end
def self.survey_users
cutoff_date = Date.parse(Rails.application.config.signup_survey_cutoff_date) # Define a cutoff date for the survey/gear setup emails
User.where("users.signup_survey_sent_at IS NULL AND users.created_at < ? AND users.created_at > ?", 1.days.ago, cutoff_date)
end
end
end

View File

@ -0,0 +1,47 @@
module JamRuby
class GearSetupReminder
@@log = Logging.logger[GearSetupReminder]
def self.send_reminders
begin
cutoff_date = Date.parse(Rails.application.config.signup_survey_cutoff_date) # Define a cutoff date for the survey/gear setup emails
reminder1_users(cutoff_date).find_each do |user|
UserMailer.gear_setup_reminder1(user).deliver_now
User.where(id: user.id).update_all(gear_setup_reminder1_sent_at: Time.now)
end
reminder2_users(cutoff_date).find_each do |user|
UserMailer.gear_setup_reminder2(user).deliver_now
User.where(id: user.id).update_all(gear_setup_reminder2_sent_at: Time.now)
end
reminder3_users(cutoff_date).find_each do |user|
UserMailer.gear_setup_reminder3(user).deliver_now
User.where(id: user.id).update_all(gear_setup_reminder3_sent_at: Time.now)
end
rescue Exception => e
@@log.error("unable to send gear setup reminder email #{e}")
puts "unable to send gear setup reminder email #{e}"
end
end
def self.prospect_users
User.where("users.first_certified_gear_at IS NULL")
end
def self.reminder1_users(cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at < ? AND users.created_at > ? AND users.gear_setup_reminder1_sent_at IS NULL", 1.day.ago, cutoff_date)
end
def self.reminder2_users(cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at < ? AND users.created_at > ? AND users.gear_setup_reminder1_sent_at IS NOT NULL AND users.gear_setup_reminder2_sent_at IS NULL", 3.days.ago, cutoff_date)
end
def self.reminder3_users(cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at < ? AND users.created_at > ? AND users.gear_setup_reminder2_sent_at IS NOT NULL AND users.gear_setup_reminder3_sent_at IS NULL", 5.days.ago, cutoff_date)
end
end
end

View File

@ -863,6 +863,10 @@ module JamRuby
self.save!(:validate => false)
end
def in_session?(user)
self.users.exists?(user.id)
end
def connected_participant_count
Connection.where(:music_session_id => self.id,
:aasm_state => Connection::CONNECT_STATE.to_s,

View File

@ -75,6 +75,11 @@ module JamRuby
has_stream_mix
end
def can_stop?(user)
# only allow the starting-user to create (ideally, perhaps, only the client that did it)
user == owner
end
# this should be a has-one relationship. until this, this is easiest way to get from recording > mix
def mix
self.mixes[0] if self.mixes.length > 0
@ -214,7 +219,7 @@ module JamRuby
def has_access?(user)
return false if user.nil?
users.exists?(user.id) || attached_with_lesson(user) #|| plays.where("player_id=?", user).count != 0
users.exists?(user.id) || attached_with_lesson(user) || (music_session && music_session.in_session?(user))
end
def attached_with_lesson(user)

View File

@ -420,6 +420,12 @@ module JamRuby
User.where(id: self.id).update_all(updates)
end
# check if the profile looks complete
if musician_instruments.length > 0 || genre_players.length > 0
User.where(id: self.id).update_all(profile_completed_at: Time.now)
end
end
def self.hourly_check

View File

@ -13,6 +13,9 @@ module JamRuby
#TeacherPayment.hourly_check
User.hourly_check
AffiliatePartner.tally_up(Date.today)
EmailProfileReminder.send_reminders
EmailSignupSurvey.send_survey
GearSetupReminder.send_reminders
ConnectionManager.new.cleanup_dangling
@@log.info("done")

View File

@ -0,0 +1,15 @@
module JamRuby
class ProfileReminderEmailer
extend Resque::Plugins::JamLonelyJob
@queue = :scheduled_profile_reminder_emailer
@@log = Logging.logger[ProfileReminderEmailer]
def self.perform
@@log.debug("waking up")
EmailProfileReminder.send_reminders
@@log.debug("done")
end
end
end

View File

@ -14,6 +14,7 @@
//= require react-components/actions/VideoActions
//= require client_init
//= require utils
//= require subscription_utils
//= require jamkazam
//= require modern/JamServer_copy

View File

@ -187,6 +187,11 @@ class ApiRecordingsController < ApiController
def stop
# only allow the creator to stop the recording
if @recording.can_stop?(current_user) == false
raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
@recording.stop
if @recording.errors.any?

View File

@ -386,6 +386,7 @@ if defined?(Bundler)
config.video_available = "full"
config.alerts_api_enabled = true
config.show_recording_debug_status = false
config.gear_check_ignore_high_latency = false
config.remove_whitespace_credit_card = false
config.estimate_taxes = true
@ -519,6 +520,8 @@ if defined?(Bundler)
config.spa_origin_url = "http://beta.jamkazam.local:4000"
config.user_match_monitoring_email = "user_match_monitoring_email@jamkazam.com"
config.send_user_match_mail_only_to_jamkazam_team = true
config.signup_survey_url = "https://www.surveymonkey.com/r/WVBKLYL"
config.signup_survey_cutoff_date = "2025-06-10"
config.action_mailer.asset_host = config.action_controller.asset_host
end
end

View File

@ -126,4 +126,6 @@ SampleApp::Application.configure do
config.action_controller.asset_host = 'http://www.jamkazam.local:3000'
config.send_user_match_mail_only_to_jamkazam_team = false
config.signup_survey_url = "https://www.surveymonkey.com/r/WVBKLYL"
config.signup_survey_cutoff_date = "2025-06-10"
end

View File

@ -31,5 +31,6 @@ Gon.global.braintree_token = Rails.application.config.braintree_token
Gon.global.paypal_admin_only = Rails.application.config.paypal_admin_only
Gon.global.use_video_conferencing_server = Rails.application.config.use_video_conferencing_server
Gon.global.manual_override_installer_ends_with = Rails.application.config.manual_override_installer_ends_with
Gon.global.show_recording_debug_status = Rails.application.config.show_recording_debug_status
Gon.global.env = Rails.env
Gon.global.version = ::JamWeb::VERSION

View File

@ -71,3 +71,83 @@ en:
paragraph2: "Your account was imported for you, so you'll need to set a password. Click the button below to set a password for your JamKazam account."
paragraph3: "Thanks for joining JamKazam!"
signature: "The JamKazam Team"
profile_complete_reminder1:
subject: "Update your JamKazam profile now & get connected to other musicians"
greeting: "Hello"
paragraph1: "If you may be interested in playing music live online with other musicians, the first thing you should do is fill out your JamKazam profile. The information in your profile lets other musicians with similar interests find, message, and connect with you, and also lets JamKazams matchmaking features give you recommendations for great musical connections. Click the button below to update your profile now. It only takes 2 minutes to complete your profile."
update_profile: "Update Profile"
paragraph2: "After you have updated your profile, the next thing you should do is download, install, and run the JamKazam app. Youll need this app to play online with others, and the app also gives you access to more advanced JamTracks features if you signed up to use our huge catalog of backing tracks. After you download and install the app, we recommend you open the app and then leave the app running for about 10-15 minutes on your computer. The app will use this time to quietly collect Internet data that we use to figure out which other musicians on JamKazam youll have the best connections with i.e. the lowest latency (or lag) when playing together"
paragraph3: "Click a button below to go to the JamKazam app download page. (Most users should click the Download App button, but if youre on a Mac that cant run MacOS 10.15 or later, click the Download Legacy App button, as that version of the app supports older MacOS versions back to 10.7.) After you download it, double click the downloaded installer, and follow the on-screen instructions to install the app, then start the app and leave it running for about 15 minutes."
download_app: "Download App"
download_legacy_app: "Download Legacy App"
paragraph4: "For next steps, dont forget you can always refer back to the Welcome email we sent you when you signed up. And if you run into any problems or get stuck, please send us email at support@jamkazam.com. Were always happy to help"
regards: "Best Regards,"
signature: "JamKazam Team"
profile_complete_reminder2:
subject: "Complete your JamKazam profile"
greeting: "Hello"
paragraph1: "We share your profile with JamKazam members who may be a good fit to play music with you during your first week on the platform via an automated email feature. Dont miss this opportunity to connect. Click the button below to update your profile now, before its shared with others!"
update_profile: "Update Profile"
paragraph2: "For next steps after your profile, dont forget you can always refer back to the Welcome email we sent you when you signed up. And if you run into any problems or get stuck, please send us email at support@jamkazam.com. Were always happy to help!"
regards: "Best Regards,"
signature: "JamKazam Team"
profile_complete_reminder3:
subject: "Complete your JamKazam profile"
greeting: "Hello"
paragraph1: "Your profile is your key to connecting with other musicians on JamKazam. It lets others know what instruments you play at what level of proficiency and what kinds of music you like to play. This lets our existing community find and reach out to you to connect and play, and this information also powers our automated matchmaking features that make recommendations to you. If you think you might want to play music live online on JamKazam, please click the button below now to fill out your profile!"
update_profile: "Update Profile"
regards: "Best Regards,"
signature: "JamKazam Team"
gear_setup_reminder1:
subject: "Set up audio gear in JamKazam now so you can get in your first session"
greeting: "Hello"
paragraph1: "The next thing you should do with JamKazam is set up your audio gear and test it in a solo session by yourself."
paragraph2: |
If you already have an audio interface and the ability to connect your computer to your home router using an Ethernet cable, you are ready to set up your gear. You can find all of our gear setup articles <a href="https://jamkazam.freshdesk.com/support/solutions/66000073844">here</a>. We recommend you use our articles on setting up your audio interface <a href="https://jamkazam.freshdesk.com/support/solutions/folders/66000108387">for Mac</a> or <a href="https://jamkazam.freshdesk.com/support/solutions/folders/66000108430">for Windows</a> to make sure you get this critical step done properly, and that you use our article on <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000124756">connecting your computer via Ethernet</a>, which is the other critical setup step.
paragraph3: |
If youre not sure what gear you need, we recommend you start by reading <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000122533-what-gear-do-i-need-to-play-on-jamkazam-">this article</a> that explains this topic in general terms. Next, <a href="https://jamkazam.freshdesk.com/support/solutions/folders/66000108418">check this list of articles</a> to find the one that best describes you. You need to use an audio interface rather than relying on the built-in mic on your computer, and you need to connect your computer to your Internet router using an Ethernet cable rather than using WiFi. <a href="https://jamkazam.freshdesk.com/support/solutions/folders/66000108419">See this list of articles</a> for recommendations on gear. If youre worried about spending money on gear without knowing how well JamKazam will work for you, you can buy gear on Amazon, try it for a week, and return it for a refund if youre not happy for a risk-free trial experience.
paragraph4: |
If you have any trouble or feel confused about gear setup, you can email us for help at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a>. You can also visit with a JamKazam support team member in our <a href="https://us02web.zoom.us/j/5967470315?pwd=eHZZL2hmVW1haUU5aTZTUUJobjFIdz09">weekly Zoom office hours</a>, which is offered every Wednesday from 11am to 12pm US Central Time.
regards: "Best Regards,"
signature: "JamKazam Team"
gear_setup_reminder2:
subject: "Set up your gear in JamKazam now to connect with other musicians"
greeting: "Hello"
paragraph1: "We share your profile with JamKazam members who may be a good fit to play music with you during your first week on the platform via an automated email feature. Dont miss this opportunity to get connected!"
paragraph2: "One of the critical pieces of information in getting you connected is your latency to other musicians on JamKazam (this is the amount of time it takes to get your audio to them). To get this information, you need to set up your gear in the JamKazam app."
paragraph3: |
If you already have an audio interface and the ability to connect your computer to your home router using an Ethernet cable, you are ready to set up your gear. You can find all of our gear setup articles <a href="https://jamkazam.freshdesk.com/support/solutions/66000073844">here</a>. We recommend you use our articles on setting up your audio interface <a href="https://jamkazam.freshdesk.com/support/solutions/folders/66000108387">for Mac</a> or <a href="https://jamkazam.freshdesk.com/support/solutions/folders/66000108430">for Windows</a> to make sure you get this critical step done properly, and that you use our article on <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000124756">connecting your computer via Ethernet</a>, which is the other critical setup step.
paragraph4: |
If youre not sure what gear you need, we recommend you start by reading <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000122533-what-gear-do-i-need-to-play-on-jamkazam-">this article</a> that explains this topic in general terms. Next, <a href="https://jamkazam.freshdesk.com/support/solutions/folders/66000108418">check this list of articles</a> to find the one that best describes you. You need to use an audio interface rather than relying on the built-in mic on your computer, and you need to connect your computer to your Internet router using an Ethernet cable rather than using WiFi. <a href="https://jamkazam.freshdesk.com/support/solutions/folders/66000108419">See this list of articles</a> for recommendations on gear. If youre worried about spending money on gear without knowing how well JamKazam will work for you, you can buy gear on Amazon, try it for a week, and return it for a refund if youre not happy for a risk-free trial experience.
paragraph5: |
If you have any trouble or feel confused about gear setup, you can email us for help at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a>. You can also visit with a JamKazam support team member in our <a href="https://us02web.zoom.us/j/5967470315?pwd=eHZZL2hmVW1haUU5aTZTUUJobjFIdz09">weekly Zoom office hours</a>, which is offered every Wednesday from 11am to 12pm US Central Time.
regards: "Best Regards,"
signature: "JamKazam Team"
gear_setup_reminder3:
subject: Dont waste your free 30-day premium gold plan - set up your gear now!
greeting: "Hello"
paragraph1: |
When you sign up for JamKazam, we give you a free 30-day premium gold plan so you can fully experience how amazing our online sessions are and how JamKazam can help you play more music with more people to bring more musical joy to your life.
paragraph2: |
Dont waste your 30-day window! Set up your gear in the JamKazam app now, and get started.
paragraph3: |
If you already have an audio interface and the ability to connect your computer to your home router using an Ethernet cable, you are ready to set up your gear. You can find all of our gear setup articles <a href="https://jamkazam.freshdesk.com/support/solutions/66000073844">here</a>. We recommend you use our articles on setting up your audio interface <a href="https://jamkazam.freshdesk.com/support/solutions/folders/66000108387">for Mac</a> or <a href="https://jamkazam.freshdesk.com/support/solutions/folders/66000108430">for Windows</a> to make sure you get this critical step done properly, and that you use our article on <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000124756">connecting your computer via Ethernet</a>, which is the other critical setup step.
paragraph4: |
If youre not sure what gear you need, we recommend you start by reading <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000122533-what-gear-do-i-need-to-play-on-jamkazam-">this article</a> that explains this topic in general terms. Next, <a href="https://jamkazam.freshdesk.com/support/solutions/folders/66000108418">check this list of articles</a> to find the one that best describes you. You need to use an audio interface rather than relying on the built-in mic on your computer, and you need to connect your computer to your Internet router using an Ethernet cable rather than using WiFi. <a href="https://jamkazam.freshdesk.com/support/solutions/folders/66000108419">See this list of articles</a> for recommendations on gear. If youre worried about spending money on gear without knowing how well JamKazam will work for you, you can buy gear on Amazon, try it for a week, and return it for a refund if youre not happy for a risk-free trial experience.
paragraph5: |
If you have any trouble or feel confused about gear setup, you can email us for help at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a>. You can also visit with a JamKazam support team member in our <a href="https://us02web.zoom.us/j/5967470315?pwd=eHZZL2hmVW1haUU5aTZTUUJobjFIdz09">weekly Zoom office hours</a>, which is offered every Wednesday from 11am to 12pm US Central Time.
regards: "Best Regards,"
signature: "JamKazam Team"
signup_survey:
subject: "Let us help you to be successful on JamKazam"
greeting: "Hi"
paragraph1: "Thanks for signing up to join our community of musicians! Please click the link below and take 2 minutes to let us know a little more about your goals. Our support team will use this information to provide tailored, individual help to you to make it faster and easier for you to be successful."
ps: "p.s."
ps_text: "If we can help in any way, please always feel free to contact us at"
regards: "Best Regards,"
signature: "JamKazam Team"