diff --git a/jam-ui/cypress/e2e/friends/friends-page.cy.js b/jam-ui/cypress/e2e/friends/friends-page.cy.js index e9b6bcc9f..5985c2856 100644 --- a/jam-ui/cypress/e2e/friends/friends-page.cy.js +++ b/jam-ui/cypress/e2e/friends/friends-page.cy.js @@ -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=20/, { fixture: 'people_page3' }).as('getPeople_page3'); cy.intercept('GET', /\S+\/profile/, { fixture: 'person' }); + cy.intercept('GET', /\S+\/my_notifications\S+/, { + statusCode: 200, + body: [] + }); }); describe('listing users', () => { @@ -116,7 +120,6 @@ describe('Friends page with data', () => { cy.wait('@getPeople_page2') cy.get('[data-testid=paginate-next-page]').click(); 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=peopleListTable] > tbody tr').should('have.length', 30); cy.get('[data-testid=paginate-next-page]').should('not.exist'); diff --git a/jam-ui/cypress/e2e/sessions/browse-sessions-page.cy.js b/jam-ui/cypress/e2e/sessions/browse-sessions-page.cy.js index eb48a2b03..7cae42b8c 100644 --- a/jam-ui/cypress/e2e/sessions/browse-sessions-page.cy.js +++ b/jam-ui/cypress/e2e/sessions/browse-sessions-page.cy.js @@ -19,7 +19,9 @@ describe('Browse sessions', () => { }); 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'); }); }); diff --git a/jam-ui/cypress/support/e2e.js b/jam-ui/cypress/support/e2e.js index 3a28a93ed..b9173a41b 100644 --- a/jam-ui/cypress/support/e2e.js +++ b/jam-ui/cypress/support/e2e.js @@ -27,6 +27,26 @@ beforeEach(() => { statusCode: 200, body: [], }).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'); + }); diff --git a/jam-ui/src/components/notification/JKNotification.js b/jam-ui/src/components/notification/JKNotification.js index a41e5c87b..553337148 100644 --- a/jam-ui/src/components/notification/JKNotification.js +++ b/jam-ui/src/components/notification/JKNotification.js @@ -9,6 +9,7 @@ import JKGenericNotification from './JKGenericNotification'; import JKFriendRequestNotification from './JKFriendRequestNotification'; import TextMessageNotification from './JKTextMessageNotification'; import classNames from 'classnames'; +import { toast } from 'react-toastify'; function JKNotification(props) { @@ -16,11 +17,8 @@ function JKNotification(props) { const dispatch = useDispatch(); const { currentUser } = useAuth() - const handleOnAccept = () => { - deleteNotification(); - } - - const deleteNotification = async () => { + + const handleOnAccept = async () => { const options = { userId: currentUser.id, notificationId: notification_id @@ -29,13 +27,15 @@ function JKNotification(props) { await dispatch(removeNotification(options)) .unwrap() .then(resp => { - + toast.success('Friend request accepted'); }) .catch(error => { - + toast.error('Error accepting friend request'); }) } + + const NOTIFICATION_TYPES = { TEXT_MESSAGE: 'TEXT_MESSAGE', FRIEND_REQUEST: 'FRIEND_REQUEST' diff --git a/jam-ui/src/components/page/JKMusicSessions.js b/jam-ui/src/components/page/JKMusicSessions.js index fe018781c..e2d14bc91 100644 --- a/jam-ui/src/components/page/JKMusicSessions.js +++ b/jam-ui/src/components/page/JKMusicSessions.js @@ -1,5 +1,6 @@ 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 { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; @@ -53,9 +54,9 @@ function JKMusicSessions() { ) : ( - - {t('no_records', { ns: 'common' })} - + {t('list.no_records_1', { ns: 'sessions' })} + {t('list.create_session', { ns: 'sessions' })} + {t('list.no_records_2', { ns: 'sessions' })} )} diff --git a/jam-ui/src/components/page/JKPeopleFilter.js b/jam-ui/src/components/page/JKPeopleFilter.js index 3bf8f6ba7..f24769b30 100644 --- a/jam-ui/src/components/page/JKPeopleFilter.js +++ b/jam-ui/src/components/page/JKPeopleFilter.js @@ -14,7 +14,7 @@ import JKPeopleSwiper from './JKPeopleSwiper'; import { getGenres, getInstruments } from '../../helpers/rest'; import { useForm, Controller, useFormState } from 'react-hook-form'; 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() { const { t } = useTranslation(); @@ -35,11 +35,10 @@ function JKPeopleFilter() { const peopleListRef = useRef(); - const people = useSelector(state => state.people.people); - //const hasOffset = useSelector(state => state.people.hasOffset); - const offset = useSelector(state => state.people.offset); - const loadingStatus = useSelector(state => state.people.status); - const prefetched = useSelector(state => state.people.prefetched) + const people = useSelector(state => state.musician.people); + const offset = useSelector(state => state.musician.offset); + const loadingStatus = useSelector(state => state.musician.status); + const prefetched = useSelector(state => state.musician.prefetched) const { register, handleSubmit, setValue, getValues, control } = useForm({ defaultValues: { diff --git a/jam-ui/src/i18n/locales/en/sessions.json b/jam-ui/src/i18n/locales/en/sessions.json index e26188e3c..8ef477b49 100644 --- a/jam-ui/src/i18n/locales/en/sessions.json +++ b/jam-ui/src/i18n/locales/en/sessions.json @@ -37,7 +37,10 @@ "notes": { "invited": "YOU WERE INVITED TO 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": { "page_title": "Lobby", diff --git a/jam-ui/src/store/features/musiciansSlice.js b/jam-ui/src/store/features/musiciansSlice.js new file mode 100644 index 000000000..796b35540 --- /dev/null +++ b/jam-ui/src/store/features/musiciansSlice.js @@ -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; \ No newline at end of file diff --git a/jam-ui/src/store/features/peopleSlice.js b/jam-ui/src/store/features/peopleSlice.js index f73ca694f..dff91a503 100644 --- a/jam-ui/src/store/features/peopleSlice.js +++ b/jam-ui/src/store/features/peopleSlice.js @@ -1,30 +1,31 @@ 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 = { people: [], - prefetched: [], + //prefetched: [], status: 'idel', error: null, hasOffset: false, offset: 0 } -export const fetchPeople = createAsyncThunk( - 'people/fetchPeople', - async (options, thunkAPI) => { - const response = await getPeople(options) - return response.json() - } -) +// export const fetchPeople = createAsyncThunk( +// 'people/fetchPeople', +// async (options, thunkAPI) => { +// const response = await getPeople(options) +// return response.json() +// } +// ) -export const preFetchPeople = createAsyncThunk( - 'people/preFetchPeople', - async (options, thunkAPI) => { - const response = await getPeople(options) - return response.json() - } -) +// export const preFetchPeople = createAsyncThunk( +// 'people/preFetchPeople', +// async (options, thunkAPI) => { +// const response = await getPeople(options) +// return response.json() +// } +// ) export const fetchPeopleByIds = createAsyncThunk( 'people/fetchPeopleByIds', @@ -73,60 +74,60 @@ export const peopleSlice = createSlice({ resetState: (state) => { return { ...initialState } }, - loadPrefetched: (state, action) => { - if(state.prefetched.length > 0){ - const records = [...state.people, ...state.prefetched]; - state.people = records; - } - state.prefetched = [] - } + // 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(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) => { @@ -161,6 +162,6 @@ export const peopleSlice = createSlice({ 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; \ No newline at end of file diff --git a/jam-ui/src/store/store.js b/jam-ui/src/store/store.js index fec3b7ad5..07bf6d9f9 100644 --- a/jam-ui/src/store/store.js +++ b/jam-ui/src/store/store.js @@ -2,6 +2,7 @@ import { configureStore } from "@reduxjs/toolkit" import textMessageReducer from "./features/textMessagesSlice" import lobbyChatMessagesReducer from "./features/lobbyChatMessagesSlice" import peopleReducer from "./features/peopleSlice" +import MusicianReducer from "./features/musiciansSlice" import onlineMusicianReducer from "./features/onlineMusiciansSlice" import sessionReducer from "./features/sessionsSlice" import notificationReducer from './features/notificationSlice' @@ -15,6 +16,7 @@ export default configureStore({ reducer: { textMessage: textMessageReducer, people: peopleReducer, + musician: MusicianReducer, notification: notificationReducer, session: sessionReducer, // this is the slice that holds the currently active sessions latency: latencyReducer, diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 6ac36c43b..51bec94a8 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -545,7 +545,7 @@ class ApiUsersController < ApiController limit = 20 if limit <= 0 offset = params[:offset].to_i offset = 0 if offset < 0 - query = @notifications = @user.notifications.joins(:source_user) + query = @user.notifications.joins(:source_user) @unread_total = query.unread.size @notifications = query.offset(offset).limit(limit) @next = @notifications.size > 0 ? offset + limit : nil