Merge branch 'develop' of bitbucket.org:jamkazam/jam-cloud into develop

This commit is contained in:
Seth Call 2024-10-05 15:54:26 -05:00
commit 4732138cbd
11 changed files with 286 additions and 89 deletions

View File

@ -99,6 +99,10 @@ describe('Friends page with data', () => {
cy.intercept('POST', /\S+\/filter\?offset=10/, { fixture: 'people_page2' }).as('getPeople_page2'); cy.intercept('POST', /\S+\/filter\?offset=10/, { fixture: 'people_page2' }).as('getPeople_page2');
cy.intercept('POST', /\S+\/filter\?offset=20/, { fixture: 'people_page3' }).as('getPeople_page3'); cy.intercept('POST', /\S+\/filter\?offset=20/, { fixture: 'people_page3' }).as('getPeople_page3');
cy.intercept('GET', /\S+\/profile/, { fixture: 'person' }); cy.intercept('GET', /\S+\/profile/, { fixture: 'person' });
cy.intercept('GET', /\S+\/my_notifications\S+/, {
statusCode: 200,
body: []
});
}); });
describe('listing users', () => { describe('listing users', () => {
@ -116,7 +120,6 @@ describe('Friends page with data', () => {
cy.wait('@getPeople_page2') cy.wait('@getPeople_page2')
cy.get('[data-testid=paginate-next-page]').click(); cy.get('[data-testid=paginate-next-page]').click();
cy.get('[data-testid=peopleListTable] > tbody tr').should('have.length', 20); cy.get('[data-testid=peopleListTable] > tbody tr').should('have.length', 20);
//cy.get('[data-testid=paginate-next-page]').should('not.exist');
cy.get('[data-testid=paginate-next-page]').click(); cy.get('[data-testid=paginate-next-page]').click();
cy.get('[data-testid=peopleListTable] > tbody tr').should('have.length', 30); cy.get('[data-testid=peopleListTable] > tbody tr').should('have.length', 30);
cy.get('[data-testid=paginate-next-page]').should('not.exist'); cy.get('[data-testid=paginate-next-page]').should('not.exist');

View File

@ -19,7 +19,9 @@ describe('Browse sessions', () => {
}); });
it('alerts when there is no records', () => { it('alerts when there is no records', () => {
cy.contains('No Records!'); cy.contains('There are no public, open sessions currently available for you to join');
cy.contains('create a session').click();
cy.url().should('include', '/sessions/new');
}); });
}); });

View File

@ -27,6 +27,26 @@ beforeEach(() => {
statusCode: 200, statusCode: 200,
body: [], body: [],
}).as('getAppFeatures'); }).as('getAppFeatures');
cy.intercept('GET', /\S+\/users\/\S+\/profile/, {
statusCode: 200,
body: {
id: 1,
name: 'Jane Doe',
email: 'jane@example.com',
},
}).as('getUserProfile');
cy.intercept('GET', /\S+\/users\/\S+\/my_notifications/, {
statusCode: 200,
body: [],
}).as('getMyNotifications');
cy.intercept('GET', /\S+\/users\/\S+\/friends/, {
statusCode: 200,
body: [],
}).as('getMyFriends');
}); });

View File

@ -9,6 +9,7 @@ import JKGenericNotification from './JKGenericNotification';
import JKFriendRequestNotification from './JKFriendRequestNotification'; import JKFriendRequestNotification from './JKFriendRequestNotification';
import TextMessageNotification from './JKTextMessageNotification'; import TextMessageNotification from './JKTextMessageNotification';
import classNames from 'classnames'; import classNames from 'classnames';
import { toast } from 'react-toastify';
function JKNotification(props) { function JKNotification(props) {
@ -16,11 +17,8 @@ function JKNotification(props) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { currentUser } = useAuth() const { currentUser } = useAuth()
const handleOnAccept = () => {
deleteNotification(); const handleOnAccept = async () => {
}
const deleteNotification = async () => {
const options = { const options = {
userId: currentUser.id, userId: currentUser.id,
notificationId: notification_id notificationId: notification_id
@ -29,13 +27,15 @@ function JKNotification(props) {
await dispatch(removeNotification(options)) await dispatch(removeNotification(options))
.unwrap() .unwrap()
.then(resp => { .then(resp => {
toast.success('Friend request accepted');
}) })
.catch(error => { .catch(error => {
toast.error('Error accepting friend request');
}) })
} }
const NOTIFICATION_TYPES = { const NOTIFICATION_TYPES = {
TEXT_MESSAGE: 'TEXT_MESSAGE', TEXT_MESSAGE: 'TEXT_MESSAGE',
FRIEND_REQUEST: 'FRIEND_REQUEST' FRIEND_REQUEST: 'FRIEND_REQUEST'

View File

@ -1,5 +1,6 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Alert, Col, Row, Card, CardBody } from 'reactstrap'; import { Col, Row, Card, CardBody } from 'reactstrap';
import { Link } from 'react-router-dom';
import FalconCardHeader from '../common/FalconCardHeader'; import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
@ -53,9 +54,9 @@ function JKMusicSessions() {
) : ( ) : (
<Row className="p-card"> <Row className="p-card">
<Col> <Col>
<Alert color="info" className="mb-0"> {t('list.no_records_1', { ns: 'sessions' })}
{t('no_records', { ns: 'common' })} <Link to="/sessions/new">{t('list.create_session', { ns: 'sessions' })}</Link>
</Alert> {t('list.no_records_2', { ns: 'sessions' })}
</Col> </Col>
</Row> </Row>
)} )}

View File

@ -14,7 +14,7 @@ import JKPeopleSwiper from './JKPeopleSwiper';
import { getGenres, getInstruments } from '../../helpers/rest'; import { getGenres, getInstruments } from '../../helpers/rest';
import { useForm, Controller, useFormState } from 'react-hook-form'; import { useForm, Controller, useFormState } from 'react-hook-form';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { fetchPeople, resetState, loadPrefetched, preFetchPeople } from '../../store/features/peopleSlice'; import { fetchPeople, resetState, loadPrefetched, preFetchPeople } from '../../store/features/musiciansSlice';
function JKPeopleFilter() { function JKPeopleFilter() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -35,11 +35,10 @@ function JKPeopleFilter() {
const peopleListRef = useRef(); const peopleListRef = useRef();
const people = useSelector(state => state.people.people); const people = useSelector(state => state.musician.people);
//const hasOffset = useSelector(state => state.people.hasOffset); const offset = useSelector(state => state.musician.offset);
const offset = useSelector(state => state.people.offset); const loadingStatus = useSelector(state => state.musician.status);
const loadingStatus = useSelector(state => state.people.status); const prefetched = useSelector(state => state.musician.prefetched)
const prefetched = useSelector(state => state.people.prefetched)
const { register, handleSubmit, setValue, getValues, control } = useForm({ const { register, handleSubmit, setValue, getValues, control } = useForm({
defaultValues: { defaultValues: {

View File

@ -37,7 +37,10 @@
"notes": { "notes": {
"invited": "YOU WERE INVITED TO THIS SESSION", "invited": "YOU WERE INVITED TO THIS SESSION",
"has_friend": "YOU HAVE A FRIEND IN THIS SESSION" "has_friend": "YOU HAVE A FRIEND IN THIS SESSION"
} },
"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."
}, },
"lobby": { "lobby": {
"page_title": "Lobby", "page_title": "Lobby",

View File

@ -0,0 +1,166 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import { getPeople, getPeopleByIds, getPersonById, acceptFriendRequest as accept } from '../../helpers/rest';
const initialState = {
people: [],
prefetched: [],
status: 'idel',
error: null,
hasOffset: false,
offset: 0
}
export const fetchPeople = createAsyncThunk(
'musician/fetchPeople',
async (options, thunkAPI) => {
const response = await getPeople(options)
return response.json()
}
)
export const preFetchPeople = createAsyncThunk(
'musician/preFetchPeople',
async (options, thunkAPI) => {
const response = await getPeople(options)
return response.json()
}
)
export const fetchPeopleByIds = createAsyncThunk(
'musician/fetchPeopleByIds',
async (options, thunkAPI) => {
const response = await getPeopleByIds(options)
return response.json()
}
)
export const fetchPerson = createAsyncThunk(
'musician/fetchPerson',
async (options, thunkAPI) => {
const {userId} = options
const response = await getPersonById(userId)
return response.json()
}
,{
condition: (options, {getState, extra}) => {
const {people} = getState()
const {userId} = options
const person = people.people.find(person => person.id === userId)
if(person && person.website){
//only proceed if full data set for user has not been fetched. person.website is not included in the initial data fetching (i.e: in friends listing ).
return false;
}
}
}
)
export const acceptFriendRequest = createAsyncThunk(
'musician/acceptFriendRequest',
async(options) => {
const { userId, ...rest } = options
const response = await accept(userId, rest)
return response.json()
}
)
export const musicianSlice = createSlice({
name: 'musician',
initialState,
reducers: {
add: (state, action) => {
state.people.push(action.payload)
},
resetState: (state) => {
return { ...initialState }
},
loadPrefetched: (state, action) => {
if(state.prefetched.length > 0){
const records = [...state.people, ...state.prefetched];
state.people = records;
}
state.prefetched = []
}
},
extraReducers: (builder) => {
builder
.addCase(fetchPeople.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchPeople.fulfilled, (state, action) => {
// const records = [...state.people, ...action.payload.musicians];
// state.people = records
// state.hasOffset = !!action.payload.offset
// state.offset = action.payload.offset
// state.status = 'succeeded'
//---
const records = new Set([...state.people, ...action.payload.musicians]);
const unique = [];
records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x))
state.people = unique
state.hasOffset = !!action.payload.offset
state.offset = action.payload.offset
state.status = 'succeeded'
})
.addCase(fetchPeople.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
.addCase(preFetchPeople.pending, (state, action) => {
state.status = 'loading'
})
.addCase(preFetchPeople.fulfilled, (state, action) => {
// const records = [...state.prefetched, ...action.payload.musicians];
// state.prefetched = records
// state.hasOffset = !!action.payload.offset
// state.offset = action.payload.offset
// state.status = 'succeeded'
//---
const records = new Set([...state.prefetched, ...action.payload.musicians]);
const unique = [];
records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x))
state.people = unique
state.hasOffset = !!action.payload.offset
state.offset = action.payload.offset
state.status = 'succeeded'
})
.addCase(preFetchPeople.rejected, (state, action) => {
state.error = action.error.message
state.status = 'failed'
})
.addCase(acceptFriendRequest.fulfilled, (state, action) => {
})
.addCase(fetchPerson.fulfilled, (state, action) => {
const person = state.people.find(person => person.id === action.payload.id)
if(person){
const updated = {
...person,
...action.payload
}
const objIndex = state.people.findIndex((p => p.id === updated.id));
state.people[objIndex] = updated
}else{
state.people.push(action.payload)
}
})
.addCase(fetchPeopleByIds.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchPeopleByIds.fulfilled, (state, action) => {
const records = new Set([...state.people, ...action.payload.musicians]);
const unique = [];
records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x))
state.people = unique
state.status = 'succeeded'
})
.addCase(fetchPeopleByIds.rejected, (state, action) => {
state.error = action.error.message
state.status = 'failed'
})
}
})
export const selectPersonById = (state, userId) => state.people.find((person) => person.id === userId)
export const { add, resetState, loadPrefetched } = musicianSlice.actions;
export default musicianSlice.reducer;

View File

@ -1,30 +1,31 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit" import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import { getPeople, getPeopleByIds, getPersonById, acceptFriendRequest as accept } from '../../helpers/rest'; //import { getPeople, getPeopleByIds, getPersonById, acceptFriendRequest as accept } from '../../helpers/rest';
import { getPeopleByIds, getPersonById, acceptFriendRequest as accept } from '../../helpers/rest';
const initialState = { const initialState = {
people: [], people: [],
prefetched: [], //prefetched: [],
status: 'idel', status: 'idel',
error: null, error: null,
hasOffset: false, hasOffset: false,
offset: 0 offset: 0
} }
export const fetchPeople = createAsyncThunk( // export const fetchPeople = createAsyncThunk(
'people/fetchPeople', // 'people/fetchPeople',
async (options, thunkAPI) => { // async (options, thunkAPI) => {
const response = await getPeople(options) // const response = await getPeople(options)
return response.json() // return response.json()
} // }
) // )
export const preFetchPeople = createAsyncThunk( // export const preFetchPeople = createAsyncThunk(
'people/preFetchPeople', // 'people/preFetchPeople',
async (options, thunkAPI) => { // async (options, thunkAPI) => {
const response = await getPeople(options) // const response = await getPeople(options)
return response.json() // return response.json()
} // }
) // )
export const fetchPeopleByIds = createAsyncThunk( export const fetchPeopleByIds = createAsyncThunk(
'people/fetchPeopleByIds', 'people/fetchPeopleByIds',
@ -73,60 +74,60 @@ export const peopleSlice = createSlice({
resetState: (state) => { resetState: (state) => {
return { ...initialState } return { ...initialState }
}, },
loadPrefetched: (state, action) => { // loadPrefetched: (state, action) => {
if(state.prefetched.length > 0){ // if(state.prefetched.length > 0){
const records = [...state.people, ...state.prefetched]; // const records = [...state.people, ...state.prefetched];
state.people = records; // state.people = records;
} // }
state.prefetched = [] // state.prefetched = []
} // }
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder builder
.addCase(fetchPeople.pending, (state, action) => { // .addCase(fetchPeople.pending, (state, action) => {
state.status = 'loading' // state.status = 'loading'
}) // })
.addCase(fetchPeople.fulfilled, (state, action) => { // .addCase(fetchPeople.fulfilled, (state, action) => {
// const records = [...state.people, ...action.payload.musicians]; // // const records = [...state.people, ...action.payload.musicians];
// state.people = records // // state.people = records
// state.hasOffset = !!action.payload.offset // // state.hasOffset = !!action.payload.offset
// state.offset = action.payload.offset // // state.offset = action.payload.offset
// state.status = 'succeeded' // // state.status = 'succeeded'
//--- // //---
const records = new Set([...state.people, ...action.payload.musicians]); // const records = new Set([...state.people, ...action.payload.musicians]);
const unique = []; // const unique = [];
records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x)) // records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x))
state.people = unique // state.people = unique
state.hasOffset = !!action.payload.offset // state.hasOffset = !!action.payload.offset
state.offset = action.payload.offset // state.offset = action.payload.offset
state.status = 'succeeded' // state.status = 'succeeded'
}) // })
.addCase(fetchPeople.rejected, (state, action) => { // .addCase(fetchPeople.rejected, (state, action) => {
state.status = 'failed' // state.status = 'failed'
state.error = action.error.message // state.error = action.error.message
}) // })
.addCase(preFetchPeople.pending, (state, action) => { // .addCase(preFetchPeople.pending, (state, action) => {
state.status = 'loading' // state.status = 'loading'
}) // })
.addCase(preFetchPeople.fulfilled, (state, action) => { // .addCase(preFetchPeople.fulfilled, (state, action) => {
// const records = [...state.prefetched, ...action.payload.musicians]; // // const records = [...state.prefetched, ...action.payload.musicians];
// state.prefetched = records // // state.prefetched = records
// state.hasOffset = !!action.payload.offset // // state.hasOffset = !!action.payload.offset
// state.offset = action.payload.offset // // state.offset = action.payload.offset
// state.status = 'succeeded' // // state.status = 'succeeded'
//--- // //---
const records = new Set([...state.prefetched, ...action.payload.musicians]); // const records = new Set([...state.prefetched, ...action.payload.musicians]);
const unique = []; // const unique = [];
records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x)) // records.map(x => unique.filter(p => p.id === x.id).length > 0 ? null : unique.push(x))
state.people = unique // state.people = unique
state.hasOffset = !!action.payload.offset // state.hasOffset = !!action.payload.offset
state.offset = action.payload.offset // state.offset = action.payload.offset
state.status = 'succeeded' // state.status = 'succeeded'
}) // })
.addCase(preFetchPeople.rejected, (state, action) => { // .addCase(preFetchPeople.rejected, (state, action) => {
state.error = action.error.message // state.error = action.error.message
state.status = 'failed' // state.status = 'failed'
}) // })
.addCase(acceptFriendRequest.fulfilled, (state, action) => { .addCase(acceptFriendRequest.fulfilled, (state, action) => {
}) })
.addCase(fetchPerson.fulfilled, (state, action) => { .addCase(fetchPerson.fulfilled, (state, action) => {
@ -161,6 +162,6 @@ export const peopleSlice = createSlice({
export const selectPersonById = (state, userId) => state.people.find((person) => person.id === userId) export const selectPersonById = (state, userId) => state.people.find((person) => person.id === userId)
export const { add, resetState, loadPrefetched } = peopleSlice.actions; export const { add, resetState } = peopleSlice.actions;
export default peopleSlice.reducer; export default peopleSlice.reducer;

View File

@ -2,6 +2,7 @@ import { configureStore } from "@reduxjs/toolkit"
import textMessageReducer from "./features/textMessagesSlice" import textMessageReducer from "./features/textMessagesSlice"
import lobbyChatMessagesReducer from "./features/lobbyChatMessagesSlice" import lobbyChatMessagesReducer from "./features/lobbyChatMessagesSlice"
import peopleReducer from "./features/peopleSlice" import peopleReducer from "./features/peopleSlice"
import MusicianReducer from "./features/musiciansSlice"
import onlineMusicianReducer from "./features/onlineMusiciansSlice" import onlineMusicianReducer from "./features/onlineMusiciansSlice"
import sessionReducer from "./features/sessionsSlice" import sessionReducer from "./features/sessionsSlice"
import notificationReducer from './features/notificationSlice' import notificationReducer from './features/notificationSlice'
@ -15,6 +16,7 @@ export default configureStore({
reducer: { reducer: {
textMessage: textMessageReducer, textMessage: textMessageReducer,
people: peopleReducer, people: peopleReducer,
musician: MusicianReducer,
notification: notificationReducer, notification: notificationReducer,
session: sessionReducer, // this is the slice that holds the currently active sessions session: sessionReducer, // this is the slice that holds the currently active sessions
latency: latencyReducer, latency: latencyReducer,

View File

@ -545,7 +545,7 @@ class ApiUsersController < ApiController
limit = 20 if limit <= 0 limit = 20 if limit <= 0
offset = params[:offset].to_i offset = params[:offset].to_i
offset = 0 if offset < 0 offset = 0 if offset < 0
query = @notifications = @user.notifications.joins(:source_user) query = @user.notifications.joins(:source_user)
@unread_total = query.unread.size @unread_total = query.unread.size
@notifications = query.offset(offset).limit(limit) @notifications = query.offset(offset).limit(limit)
@next = @notifications.size > 0 ? offset + limit : nil @next = @notifications.size > 0 ? offset + limit : nil