From 77359f7fc232de84db5dfe65ab4be9f20be42453 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Thu, 3 Oct 2024 04:34:03 +0530 Subject: [PATCH 1/3] friends page state use a separate redux slice for friends page add musician slice to prevent mess up with the other user objects fetched globally. people redux slice would serve as global store having user records that can be referenced elsewhere --- jam-ui/cypress/e2e/friends/friends-page.cy.js | 5 +- jam-ui/src/components/page/JKPeopleFilter.js | 11 +- jam-ui/src/store/features/musiciansSlice.js | 166 ++++++++++++++++++ jam-ui/src/store/features/peopleSlice.js | 137 ++++++++------- jam-ui/src/store/store.js | 2 + 5 files changed, 246 insertions(+), 75 deletions(-) create mode 100644 jam-ui/src/store/features/musiciansSlice.js 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/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/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, From 0377f73b9f25e233ededc3a7d363bd6ce824272c Mon Sep 17 00:00:00 2001 From: Nuwan Date: Thu, 3 Oct 2024 19:00:27 +0530 Subject: [PATCH 2/3] show notification on friend request accept --- .../src/components/notification/JKNotification.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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' From 2afd8500bdeabda1af80346221311e96086ada9c Mon Sep 17 00:00:00 2001 From: Nuwan Date: Sat, 5 Oct 2024 02:58:48 +0530 Subject: [PATCH 3/3] change browse session no records alert --- .../e2e/sessions/browse-sessions-page.cy.js | 4 +++- jam-ui/cypress/support/e2e.js | 20 +++++++++++++++++++ jam-ui/src/components/page/JKMusicSessions.js | 9 +++++---- jam-ui/src/i18n/locales/en/sessions.json | 5 ++++- web/app/controllers/api_users_controller.rb | 2 +- 5 files changed, 33 insertions(+), 7 deletions(-) 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/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/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/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