wip browse music sessions

This commit is contained in:
Nuwan 2023-10-26 17:25:02 +05:30
parent dd03c215c4
commit b3922ec025
15 changed files with 490 additions and 8 deletions

View File

@ -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)
})
})
})

View File

@ -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"
}
]

View File

@ -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;

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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
}

View File

@ -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))
})
}

View File

@ -16,5 +16,15 @@
"description_help": "If youre creating a public session, we strongly recommend that you enter a description of your session for example, what kinds of music youre 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"
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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
}
})

View File

@ -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)