wip browse music sessions
This commit is contained in:
parent
dd03c215c4
commit
b3922ec025
|
|
@ -0,0 +1,33 @@
|
|||
/// <reference types="cypress" />
|
||||
|
||||
describe('Browse sessions', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubAuthenticate({ id: '6'})
|
||||
});
|
||||
|
||||
describe('when there are no active sessions', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, {
|
||||
body: []
|
||||
})
|
||||
cy.visit('/sessions')
|
||||
});
|
||||
|
||||
it("alerts when there is no records", () => {
|
||||
cy.contains("No Records!")
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there are active sessions', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', /\S+\/api\/sessions/, { fixture: 'sessions' })
|
||||
cy.visit('/sessions')
|
||||
});
|
||||
|
||||
it("lists the sessions", () => {
|
||||
cy.get('[data-testid=sessionsListTable] tbody tr').should('have.length', 1)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
[
|
||||
{
|
||||
"id": "df953ba-7c59-4762-8cc3-279db82e872a",
|
||||
"name": "Open Session",
|
||||
"description": "Feel free to join this session, it's open!",
|
||||
"musician_access": true,
|
||||
"approval_required": false,
|
||||
"friends_can_join": false,
|
||||
"fan_access": true,
|
||||
"fan_chat": false,
|
||||
"user_id": "29becbf4-8be5-4078-9405-0edadc9fa42d",
|
||||
"claimed_recording_initiator_id": null,
|
||||
"track_changes_counter": 0,
|
||||
"max_score": 0,
|
||||
"backing_track_path": null,
|
||||
"metronome_active": false,
|
||||
"jam_track_initiator_id": null,
|
||||
"jam_track_id": null,
|
||||
"music_session_id_int": 2210,
|
||||
"use_video_conferencing_server": false,
|
||||
"music_notations": [],
|
||||
"participants": [
|
||||
{
|
||||
"ip_address": "192.168.1.110",
|
||||
"client_id": "63cdbcf7-a3c6-49bf-9412-0c09d4a6796b",
|
||||
"joined_session_at": "2023-10-26T07:16:22.605Z",
|
||||
"id": "3c1f2a74-0ccf-4ed1-9828-ce909edf61b7",
|
||||
"metronome_open": false,
|
||||
"is_jamblaster": false,
|
||||
"client_role": "parent",
|
||||
"parent_client_id": null,
|
||||
"client_id_int": 78125,
|
||||
"tracks": [
|
||||
{
|
||||
"id": "833de71f-7bc0-4d8e-9ea6-8a695181960b",
|
||||
"connection_id": "3c1f2a74-0ccf-4ed1-9828-ce909edf61b7",
|
||||
"instrument_id": "electric guitar",
|
||||
"sound": "stereo",
|
||||
"client_track_id": "FWAPMulti_2_10200m",
|
||||
"client_resource_id": "FWAPMulti_2_10200",
|
||||
"updated_at": "2023-10-26T07:16:22.611Z",
|
||||
"instrument": "Electric Guitar"
|
||||
}
|
||||
],
|
||||
"backing_tracks": [],
|
||||
"user": {
|
||||
"id": "29becbf4-8be5-4078-9405-0edadc9fa42d",
|
||||
"photo_url": null,
|
||||
"name": "Nuwan Chathuranga",
|
||||
"is_friend": true,
|
||||
"connection_state": "connected",
|
||||
"subscription": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"invitations": [
|
||||
{
|
||||
"id": "d49f3c07-7f49-4dad-8de1-2020046438de",
|
||||
"sender_id": "29becbf4-8be5-4078-9405-0edadc9fa42d",
|
||||
"receiver_id": "27bd4a30-d1b8-4eea-8454-01a104d59381"
|
||||
}
|
||||
],
|
||||
"lesson_session": null,
|
||||
"mount": {
|
||||
"id": "c3504b02-dc18-4d85-a562-117eaaffc136",
|
||||
"name": "/5tZz2_G8kT0-8UlUAna_yQ.mp3",
|
||||
"sourced": false,
|
||||
"listeners": 0,
|
||||
"bitrate": 128,
|
||||
"subtype": null,
|
||||
"url": "http://localhost:10000/5tZz2_G8kT0-8UlUAna_yQ.mp3",
|
||||
"mime_type": "audio/mpeg"
|
||||
},
|
||||
"can_join": true,
|
||||
"genres": [
|
||||
"Pop"
|
||||
],
|
||||
"recording": null,
|
||||
"share_url": "http://www.jamkazam.local:3000/s/T50BWPH9ICC",
|
||||
"session_controller_id": "29becbf4-8be5-4078-9405-0edadc9fa42d"
|
||||
}
|
||||
]
|
||||
|
|
@ -1,9 +1,72 @@
|
|||
import React from 'react'
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Col,
|
||||
Row,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
Table,
|
||||
Form,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter
|
||||
} from 'reactstrap';
|
||||
import FalconCardHeader from '../common/FalconCardHeader';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { fetchSessions } from '../../store/features/sessionsSlice';
|
||||
import SessionRow from '../sessions/SessionRow';
|
||||
import Loader from '../common/Loader';
|
||||
import { isIterableArray } from '../../helpers/utils';
|
||||
|
||||
function JKMusicSessions() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const sessions = useSelector(state => state.session.sessions);
|
||||
const loadingStatus = useSelector(state => state.session.status);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchSessions());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>Music Sessions Listing</div>
|
||||
)
|
||||
<Card>
|
||||
<FalconCardHeader title={t('list.page_title', { ns: 'sessions' })} titleClass="font-weight-bold" />
|
||||
<CardBody className="pt-0">
|
||||
<Table striped bordered className="fs--1" data-testid="sessionsListTable">
|
||||
<thead className="bg-200 text-900">
|
||||
<tr>
|
||||
<th scope="col">{t('list.header.session', { ns: 'sessions' })}</th>
|
||||
<th scope="col" style={{ minWidth: 250 }}>
|
||||
{t('list.header.musicians', { ns: 'sessions' })}
|
||||
</th>
|
||||
<th scope="col">{t('list.header.latency', { ns: 'sessions' })}</th>
|
||||
<th scope="col">{t('list.header.instruments', { ns: 'sessions' })}</th>
|
||||
<th scope="col">{t('actions', { ns: 'common' })}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="list">
|
||||
{loadingStatus === 'loading' && sessions.length === 0 ? (
|
||||
<Loader />
|
||||
) : isIterableArray(sessions) ? (
|
||||
sessions.map(session => <SessionRow key={session.id} session={session} />)
|
||||
) : (
|
||||
<Row className="p-card">
|
||||
<Col>
|
||||
<Alert color="info" className="mb-0">
|
||||
No Records!
|
||||
</Alert>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default JKMusicSessions
|
||||
export default JKMusicSessions;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useAuth } from '../../context/UserAuth';
|
|||
import JKFriendsAutoComplete from '../people/JKFriendsAutoComplete';
|
||||
import JKSessionInviteesChips from '../people/JKSessionInviteesChips';
|
||||
import { getFriends } from '../../helpers/rest';
|
||||
import jkCustomUrlScheme from '../../helpers/jkCustomUrlScheme';
|
||||
|
||||
const privacyMap = {
|
||||
"public": 1,
|
||||
|
|
@ -80,8 +81,9 @@ const JKNewMusicSession = () => {
|
|||
//window.open jamkazam app url using custom URL scheme
|
||||
//an example URL would be: jamkazam://url=https://www.jamkazam.com/client#/createSession/privacy~2|description~hello|inviteeIds~1,2,3,4
|
||||
const q = `privacy~${payload.privacy}|description~${payload.description}|inviteeIds~${payload.inviteeIds}`
|
||||
const url = encodeURI(`${process.env.REACT_APP_CLIENT_BASE_URL}/client#/createSession/${q}`)
|
||||
const urlScheme = `jamkazam://url=${url}`
|
||||
//const url = encodeURI(`${process.env.REACT_APP_CLIENT_BASE_URL}/client#/createSession/${q}`)
|
||||
//const urlScheme = `jamkazam://url=${url}`
|
||||
const urlScheme = jkCustomUrlScheme('createSession', q)
|
||||
window.open(urlScheme)
|
||||
try {
|
||||
//store this payload in localstorage.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import jkCustomUrlScheme from '../../helpers/jkCustomUrlScheme';
|
||||
import JKProfileAvatar from '../profile/JKProfileAvatar';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useAuth } from '../../context/UserAuth';
|
||||
import SessionUserLatency from './SessionUserLatency';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { fetchUserLatencies } from '../../store/features/latencySlice';
|
||||
import SessionUser from './SessionUser';
|
||||
|
||||
function SessionRow({session}) {
|
||||
const { currentUser } = useAuth();
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
const participantIds = session.participants.map(p => p.user.id)
|
||||
const options = { currentUserId: currentUser.id, participantIds }
|
||||
dispatch(fetchUserLatencies(options))
|
||||
}, [])
|
||||
|
||||
function joinSession(){
|
||||
const q = `joinSessionId~${session.id}`
|
||||
const urlScheme = jkCustomUrlScheme('findSession', q)
|
||||
window.open(urlScheme)
|
||||
}
|
||||
|
||||
const sessionDescription = (session) => {
|
||||
if(session.description){
|
||||
return session.description;
|
||||
}else if(session.musician_access && !session.approval_required){
|
||||
return "Public, open session. Feel free to join!"
|
||||
}else if(session.musician_access && session.approval_required){
|
||||
return "Private session. Click the enter button in the right column to request to join"
|
||||
}else if(!session.musician_access && !session.approval_required){
|
||||
return "Only RSVP musicians may join"
|
||||
}
|
||||
}
|
||||
|
||||
const invitedNote = (session) => {
|
||||
if(session.invitations.find(i => i.receiver_id === currentUser.id)){
|
||||
return "YOU WERE INVITED TO THIS SESSION"
|
||||
}
|
||||
}
|
||||
|
||||
const hasFriendNote = session => {
|
||||
if(session.participants.find(p => p.user.is_friend)){
|
||||
return "YOU HAVE A FRIEND IN THIS SESSION"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={session.id}>
|
||||
<td>
|
||||
<div>
|
||||
<u>
|
||||
<small>
|
||||
<strong>{invitedNote(session)}</strong>
|
||||
</small>
|
||||
</u>
|
||||
</div>
|
||||
<div>
|
||||
<u>
|
||||
<small>{hasFriendNote(session)}</small>
|
||||
</u>
|
||||
</div>
|
||||
<div>{sessionDescription(session)}</div>
|
||||
</td>
|
||||
<td>
|
||||
{session.participants.map(participant => (
|
||||
<SessionUser key={participant.id} user={participant.user} />
|
||||
))}
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
{session.participants.map(participant => (
|
||||
<SessionUserLatency key={participant.id} user={participant.user} />
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="d-flex flex-row">
|
||||
{session.participants.map(participant =>
|
||||
participant.tracks.map(track => <div key={track.id}>{track.instrument}</div>)
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a onClick={joinSession} className="btn btn-sm btn-outline-secondary mr-1">
|
||||
<FontAwesomeIcon icon="arrow-right" />
|
||||
</a>
|
||||
<a href="" className="btn btn-sm btn-outline-secondary">
|
||||
<FontAwesomeIcon icon="volume-up" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionRow;
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import JKProfileSidePanel from '../profile/JKProfileSidePanel';
|
||||
import JKProfileAvatar from '../profile/JKProfileAvatar';
|
||||
import { fetchPerson } from '../../store/features/peopleSlice';
|
||||
|
||||
function SessionUser({ user }) {
|
||||
const dispatch = useDispatch()
|
||||
const latencyData = useSelector(state => state.latency.latencies.find(l => l.user_id === user.id));
|
||||
const [showSidePanel, setShowSidePanel] = useState(false);
|
||||
|
||||
const toggleMoreDetails = async e => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await dispatch(fetchPerson({ userId: user.id })).unwrap();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
setShowSidePanel(prev => !prev);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex flex-row">
|
||||
<div className="avatar avatar-xl">
|
||||
<JKProfileAvatar url={user.photo_url} />
|
||||
</div>
|
||||
<div className="ml-2 ms-2">
|
||||
<a href="/#" onClick={toggleMoreDetails}>{user.name}</a>
|
||||
</div>
|
||||
</div>
|
||||
<JKProfileSidePanel user={user} latencyData={latencyData} show={showSidePanel} setShow={setShowSidePanel} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionUser;
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import JKLatencyBadge from '../profile/JKLatencyBadge';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
function SessionUserLatency({user}) {
|
||||
|
||||
const latencyData = useSelector(state => state.latency.latencies.find(l => l.user_id === user.id));
|
||||
|
||||
return <JKLatencyBadge latencyData={latencyData} />;
|
||||
}
|
||||
|
||||
export default SessionUserLatency;
|
||||
|
|
@ -156,7 +156,8 @@ import {
|
|||
faCross,
|
||||
faMusic,
|
||||
faRecordVinyl,
|
||||
faAddressCard
|
||||
faAddressCard,
|
||||
faVolumeUp
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
library.add(
|
||||
|
|
@ -279,6 +280,7 @@ library.add(
|
|||
faMusic,
|
||||
faRecordVinyl,
|
||||
faAddressCard,
|
||||
faVolumeUp,
|
||||
|
||||
// Brand
|
||||
faFacebook,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
export default (section, queryStr) => {
|
||||
const url = encodeURI(`${process.env.REACT_APP_CLIENT_BASE_URL}/client#/${section}/custom~yes|${queryStr}`)
|
||||
const urlScheme = `jamkazam://url=${url}`
|
||||
return urlScheme
|
||||
}
|
||||
|
|
@ -136,4 +136,21 @@ export const deleteNotification = (userId, notificationId) => {
|
|||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
export const getSessions = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/sessions`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
export const getLatencyToUsers = (currentUserId, participantIds) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = { user_ids: [participantIds]}
|
||||
apiFetch(`/users/${currentUserId}/latencies?${new URLSearchParams(options)}`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
|
@ -16,5 +16,15 @@
|
|||
"description_help": "If you’re creating a public session, we strongly recommend that you enter a description of your session – for example, what kinds of music you’re interested in playing. This description will be displayed next to your session in the Browse Sessions feature, which will help other musicians in the community understand if your session is a good fit for them.",
|
||||
"description_placeholder": "Enter session description. Recommended for public sessions to attract other musicians and them know what to expect in your session.",
|
||||
"create_session": "Create Session"
|
||||
},
|
||||
"list": {
|
||||
"page_title": "Browse Current Sessions",
|
||||
"header": {
|
||||
"session": "Session",
|
||||
"musicians": "Musicians",
|
||||
"latency": "Latency",
|
||||
"instruments": "Instruments",
|
||||
"actions": "Actions"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
|
||||
import { getLatencyToUsers } from "../../helpers/rest"
|
||||
|
||||
const initialState = {
|
||||
status: 'idel',
|
||||
latencies: []
|
||||
}
|
||||
|
||||
|
||||
export const fetchUserLatencies = createAsyncThunk(
|
||||
'latency/fetchUserLatencies',
|
||||
async (options, thunkAPI) => {
|
||||
const { currentUserId, participantIds } = options
|
||||
const response = await getLatencyToUsers(currentUserId, participantIds)
|
||||
return response.json()
|
||||
}
|
||||
)
|
||||
|
||||
export const latencySlice = createSlice({
|
||||
name: 'latency',
|
||||
initialState,
|
||||
reducers: {
|
||||
add: (state, action) => {
|
||||
state.latencies.push(action.payload)
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchUserLatencies.pending, (state, action) => {
|
||||
state.status = 'loading'
|
||||
})
|
||||
.addCase(fetchUserLatencies.fulfilled, (state, action) => {
|
||||
console.log("_DEBUG_ fetchUserLatencies", action.payload);
|
||||
const records = new Set([...state.latencies, ...action.payload]);
|
||||
const unique = [];
|
||||
records.map(x => unique.filter(a => a.user_id === x.user_id).length > 0 ? null : unique.push(x))
|
||||
state.latencies = unique
|
||||
})
|
||||
.addCase(fetchUserLatencies.rejected, (state, action) => {
|
||||
console.log("_DEBUG_ fail fetchUserLatencies", action.payload);
|
||||
state.status = 'failed'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export default latencySlice.reducer;
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { getSessions } from '../../helpers/rest'
|
||||
|
||||
const initialState = {
|
||||
sessions: [],
|
||||
status: 'idel',
|
||||
error: null,
|
||||
}
|
||||
|
||||
export const fetchSessions = createAsyncThunk(
|
||||
"session/fetchSessions",
|
||||
async (options, thunkAPI) => {
|
||||
const response = await getSessions();
|
||||
return response.json();
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
export const SessionSlice = createSlice({
|
||||
name: "session",
|
||||
initialState,
|
||||
reducers: {
|
||||
addSession: (state) => {},
|
||||
updateSession: (state) => {},
|
||||
deleteSession: (state) => {},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchSessions.pending, (state, action) => {
|
||||
state.status = "loading";
|
||||
})
|
||||
.addCase(fetchSessions.fulfilled, (state, action) => {
|
||||
state.status = "succeeded";
|
||||
state.sessions = action.payload;
|
||||
})
|
||||
.addCase(fetchSessions.rejected, (state, action) => {
|
||||
state.status = 'failed'
|
||||
state.error = action.error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export default SessionSlice.reducer;
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import textMessageReducer from "./features/textMessagesSlice"
|
||||
import peopleSlice from "./features/peopleSlice"
|
||||
import sessionSlice from "./features/sessionsSlice"
|
||||
import notificationSlice from './features/notificationSlice'
|
||||
import latencySlice from "./features/latencySlice"
|
||||
|
||||
export default configureStore({
|
||||
reducer: {
|
||||
textMessage: textMessageReducer,
|
||||
people: peopleSlice,
|
||||
notification: notificationSlice
|
||||
notification: notificationSlice,
|
||||
session: sessionSlice,
|
||||
latency: latencySlice
|
||||
}
|
||||
})
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
context = window
|
||||
MIX_MODES = context.JK.MIX_MODES
|
||||
AppStore = context.AppStore
|
||||
|
||||
SessionsActions = @SessionsActions
|
||||
|
||||
|
|
@ -79,8 +80,33 @@ SessionsActions = @SessionsActions
|
|||
return
|
||||
|
||||
beforeShow: () ->
|
||||
@initCustomUrlScheme()
|
||||
return
|
||||
|
||||
ensuredCallback: (sessionId) ->
|
||||
context.JK.SessionUtils.joinSession(sessionId)
|
||||
|
||||
joinSession: (sessionId) ->
|
||||
context.JK.SessionUtils.ensureValidClient(AppStore.app, context.JK.GearUtils, @ensuredCallback.bind(this, sessionId))
|
||||
|
||||
initCustomUrlScheme: () ->
|
||||
hash = decodeURIComponent(context.location.hash)
|
||||
qStr = hash.substring(hash.lastIndexOf('/') + 1)
|
||||
qParamsArr = qStr.split('|')
|
||||
isCustom = undefined
|
||||
sessionId = undefined
|
||||
qParamsArr.forEach (q) ->
|
||||
qp = q.split('~')
|
||||
if qp[0] == 'custom'
|
||||
isCustom = qp[1]
|
||||
if qp[0] == 'joinSessionId'
|
||||
sessionId = qp[1]
|
||||
if !isCustom || isCustom != 'yes'
|
||||
return
|
||||
unless sessionId
|
||||
return
|
||||
@joinSession(sessionId)
|
||||
|
||||
afterShow: () ->
|
||||
SessionsActions.watching.trigger(true)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue