initial release of my friends page

This commit is contained in:
Nuwan 2024-04-20 16:44:24 +05:30
parent 703309aa65
commit 20eb17b044
34 changed files with 1051 additions and 608 deletions

View File

@ -0,0 +1,607 @@
/// <reference types="cypress" />
const showSidePanelContent = () => {
cy.get('[data-testid=profileSidePanel] h4').should('have.text', 'Test User1');
cy.get('[data-testid=profileSidePanel] .modal-body').first().within(() => {
cy.contains('Location: Denver, CO, US')
.and('contain', 'Skill Level: Professional')
.and('contain', 'Joined JamKazam: 08-26-2021')
.and('contain', 'Last Active:')
.and('contain', 'Latency To Me:');
cy.get('.latency-badge').contains('UNKNOWN');
cy.get('[data-testid=biography]').contains('Biography of Test User1');
//instruments
cy.get('[data-testid=instruments]').contains('Acoustic Guitar: Expert');
cy.get('[data-testid=instruments]').contains('Keyboard: Expert');
//genres
cy.get('[data-testid=genres]').contains('classical, blues');
//bands
cy.get('[data-testid=bands]').contains('The Band');
//performance_samples
cy.get('[data-testid=performance_samples]').contains('Test Recording1')//.should('have.attr', 'href').and('eq', 'https://www.jamkazam.com/test-recording1');
//online presence
cy.get('[data-testid=online_presences]').contains('Youtube').should('have.attr', 'href').and('eq', 'https://www.youtube.com/testuser');
cy.get('[data-testid=online_presences]').contains('Facebook').should('have.attr', 'href').and('eq', 'https://www.facebook.com/testuser');
cy.get('[data-testid=online_presences]').contains('Twitter').should('have.attr', 'href').and('eq', 'https://www.twitter.com/testuser');
});
};
const closeSidePanel = () => {
cy.get('[data-testid=profileSidePanel] .modal-header button.close').click();
};
describe.only('Friends page without data', () => {
beforeEach(() => {
cy.stubAuthenticate();
// cy.intercept('POST', /\S+\/filter/, {
// musicians: []
// }).as("getPeople_empty");
});
it('shows no records found alert', () => {
cy.visit('/friends/my');
//default api call with from_location parameter turned off
// cy.wait('@getPeople_empty')
// .then(interception => {
// assert.isNotNull(interception.response.body, '1st API call');
// })
// .its('request.body')
// .should('deep.contain', {
// from_location: false
// });
// //now it automatically turns on from_location parameter and fetches again
// cy.wait('@getPeople_empty')
// .then(interception => {
// assert.isNotNull(interception.response.body, '2nd API call - (prefetched)');
// })
// .its('request.body')
// .should('deep.contain', {
// from_location: true
// });
// cy.contains('No Records!');
// cy.get('[data-testid=btnUpdateSearch]').click();
// cy.get('[data-testid=modalUpdateSearch] input[name=from_location]').check();
// cy.wait(1000);
// cy.get('[data-testid=btnSubmitSearch]').click();
// //default api call with from_location parameter turned on
// cy.wait('@getPeople_empty')
// .then(interception => {
// assert.isNotNull(interception.response.body, '3th API call');
// })
// .its('request.body')
// .should('deep.contain', {
// from_location: true
// });
// cy.contains('No Records!');
});
});
describe('Friends page with data', () => {
beforeEach(() => {
cy.stubAuthenticate({ id: '2' }); //currentUser id is 2 - people.yaml fixture
cy.intercept('POST', /\S+\/filter\?offset=0/, { fixture: 'people_page1' }).as('getPeople_page1');
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' });
});
describe('listing users', () => {
beforeEach(() => {
cy.visit('/friends');
});
describe('in desktop', () => {
beforeEach(() => {
cy.viewport('macbook-13');
});
it('paginate', () => {
cy.get('[data-testid=peopleListTable] > tbody tr').should('have.length', 10);
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');
});
it('show profiles', () => {
cy.contains('Find New Friends').should('exist');
cy.contains('Update Search').should('exist');
cy.contains('Reset Filters').should('exist');
cy.get('[data-testid=peopleListTable] > tbody tr')
.should('have.length', 10)
.first()
.contains('Test User1');
});
it('click profile name', () => {
//open side panel by clicking name
cy.get('[data-testid=peopleListTable]').find('.person-link').first().within(() => {
cy.contains("Test User1").click()
})
//cy.get('[data-testid=peopleListTable]').find('.person-link').first().click()
showSidePanelContent();
closeSidePanel();
});
it('click more button', () => {
//open side panel by clicking more button
cy.get('[data-testid=peopleListTable] > tbody tr')
.first()
.find('[data-testid=btnMore]')
.click();
showSidePanelContent();
closeSidePanel();
});
it('click more link', () => {
//open side panel by clicking more link
cy.get('[data-testid=peopleListTable] > tbody tr')
.first()
.find('[data-testid=linkMore]')
.click();
showSidePanelContent();
closeSidePanel();
});
it('click description more link', () => {
cy.get('[data-testid=peopleListTable] > tbody tr')
.first()
.find('td[data-testid=biography-col]')
.within(() => {
cy.contains('More').click();
});
showSidePanelContent();
closeSidePanel();
});
it('click instruments more link', () => {
cy.get('[data-testid=peopleListTable] > tbody tr')
.first()
.find('[data-testid=instrumentList]')
.within(() => {
cy.get('[data-testid=instrument]').should('have.length', 4); //show only 4 instruments plus more link
cy.contains('Acoustic Guitar: Expert');
cy.contains('Keyboard: Expert');
cy.contains('Ukulele: Expert');
cy.contains('Voice: Expert');
cy.contains('More').click();
});
showSidePanelContent();
closeSidePanel();
});
it('click genres more link', () => {
cy.get('[data-testid=peopleListTable] > tbody tr')
.first()
.find('td[data-testid=genres-col]')
.within(() => {
cy.contains('More').click();
});
showSidePanelContent();
closeSidePanel();
});
});
describe('in mobile', () => {
beforeEach(() => {
cy.viewport('iphone-6');
});
it('show profile', () => {
cy.get('[data-testid=peopleSwiper] .swiper-slide').should('have.length', 10);
cy.get('[data-testid=peopleSwiper] .swiper-slide')
.eq(0)
.contains('Test User1');
cy.get('[data-testid=peopleSwiper] .swiper-slide')
.eq(0)
.should('be.visible');
cy.get('[data-testid=peopleSwiper] .swiper-slide')
.eq(2)
.should('not.be.visible');
});
it('show all profile description', () => {
cy.get('[data-testid=peopleSwiper] .swiper-slide')
.first()
.find('[data-testid=mobBiography]')
.should('not.contain', 'More');
});
it('show all instruments', () => {
cy.get('[data-testid=peopleSwiper] .swiper-slide')
.first()
.find('[data-testid=instrumentList] div')
.its('length')
.should('be.gte', 1);
cy.get('[data-testid=peopleSwiper] .swiper-slide')
.first()
.find('[data-testid=instrumentList]')
.should('not.contain', 'More');
});
it('show all genres', () => {
cy.get('[data-testid=peopleSwiper] .swiper-slide')
.first()
.find('[data-testid=genreList] div')
.its('length')
.should('be.gte', 1);
cy.get('[data-testid=peopleSwiper] .swiper-slide')
.first()
.find('[data-testid=genreList]')
.should('not.contain', 'More');
});
//it.skip('click connect button', () => {});
//it.skip('click message button', () => {});
it.skip('paginate', () => {
cy.get('[data-testid=peopleSwiper] .swiper-button-prev').should('have.class', 'swiper-button-disabled');
for (let i = 0; i < 19; i++) {
cy.get('[data-testid=peopleSwiper] .swiper-button-next').click();
cy.wait(500);
}
cy.wait(500);
cy.get('[data-testid=peopleSwiper] .swiper-button-next').should('have.class', 'swiper-button-disabled');
});
it('click more button', () => {
cy.get('[data-testid=peopleSwiper] .swiper-slide')
.first()
.find('[data-testid=btnMore]')
.click();
showSidePanelContent();
closeSidePanel();
});
});
});
describe('making friendship', () => {
it('add friend', () => {
cy.intercept('GET', /\S+\/profile\S+/, { fixture: 'person' });
cy.intercept('POST', /\S+\/friend_requests/, { statusCode: 201, body: { ok: true } });
cy.visit('/friends');
cy.contains('Test User2').click();
cy.get('[data-testid=profileSidePanel]')
.find('[data-testid=connect]')
.should('not.be.disabled')
.click();
// cy.get('[data-testid=confirmFriendRequestModal]').within(() => {
// cy.contains('Send a friend request to Test User2');
// cy.contains('Yes').click();
// });
cy.get('[data-testid=profileSidePanel]')
.find('[data-testid=connect]')
.should('be.disabled');
cy.contains('Success! Your friend request has been sent to Test User2.');
});
it('remove friend', () => {
cy.intercept('GET', /\S+\/profile/, { fixture: 'friend' });
cy.intercept('DELETE', /\S+\/friends\S+/, { statusCode: 204, body: { ok: true } });
cy.visit('/friends');
cy.contains('Test User1').click();
cy.get('[data-testid=profileSidePanel]')
.find('[data-testid=disconnect]')
.should('exist')
.should('not.be.disabled')
.click();
cy.get('[data-testid=profileSidePanel]')
.find('[data-testid=disconnect]')
.should('not.exist');
cy.get('[data-testid=profileSidePanel]')
.find('[data-testid=connect]')
.should('be.exist')
.should('not.be.disabled');
});
});
describe('chat window', () => {
beforeEach(() => {
cy.visit('/friends');
});
it('is not disabled for friends', () => {
cy.get('[data-testid=peopleListTable] > tbody tr')
.eq(0)
.find('[data-testid=message]')
.should('not.be.disabled')
.trigger('mouseover');
cy.contains('Send a message').should('exist');
});
it('is not disabled for non friends', () => {
cy.get('[data-testid=peopleListTable] > tbody tr')
.eq(1)
.find('[data-testid=message]')
.should('not.be.disabled');
//cy.contains('You can message this user once you are friends').should('exist')
});
it('lists text messages', () => {
//initially show the most recent messages on openning chat window modal
let numberOfMessages = 10;
cy.fixture('text_messages_page1').then(json => {
cy.intercept('GET', /\S+\/text_messages\S+/, json).as('getTextMessages');
});
cy.get('[data-testid=peopleListTable] > tbody tr')
.eq(0)
.find('[data-testid=message]')
.click();
cy.wait('@getTextMessages');
cy.get('[data-testid=textMessageModal]')
.should('be.visible')
.within(() => {
cy.get('.text-message-row').should('have.length', numberOfMessages);
//display previous messages by scrolling up
const messageFixtures = ['text_messages_page2', 'text_messages_page3'];
messageFixtures.forEach(fixture => {
cy.fixture(fixture).then(json => {
cy.intercept('GET', /\S+\/text_messages\S+/, json);
cy.get('.modal-body .ScrollbarsCustom-Scroller')
.trigger('mouseover')
.scrollTo('bottom');
cy.get('.modal-body .ScrollbarsCustom-Scroller')
.trigger('mouseover')
.scrollTo('top');
numberOfMessages = numberOfMessages + 10;
cy.get('.text-message-row').should('have.length', numberOfMessages);
});
});
cy.get('button')
.contains('Close')
.should('not.be.disabled')
.click();
});
cy.get('[data-testid=textMessageModal]').should('not.be.visible');
});
it('sends message by clicking send button', () => {
cy.get('[data-testid=peopleListTable] > tbody tr')
.eq(0)
.find('[data-testid=message]')
.click();
cy.get('[data-testid=textMessageModal]').within(() => {
cy.get('button')
.contains('Send')
.should('be.disabled');
cy.get('textarea').type('Hello');
cy.get('button')
.contains('Send')
.should('not.be.disabled')
.click();
cy.get('textarea').should('have.value', '');
cy.get('button')
.contains('Send')
.should('be.disabled');
});
});
it('sends message by pressing enter key', () => {
cy.get('[data-testid=peopleListTable] > tbody tr')
.eq(0)
.find('[data-testid=message]')
.click();
cy.get('[data-testid=textMessageModal]').within(() => {
cy.get('button')
.contains('Send')
.should('be.disabled');
cy.get('textarea').type('Hello{enter}');
cy.get('textarea').should('have.value', '');
cy.get('button')
.contains('Send')
.should('be.disabled');
});
});
it('goes away by clicking close button', () => {
cy.get('[data-testid=peopleListTable] > tbody tr')
.eq(0)
.find('[data-testid=message]')
.click();
cy.get('[data-testid=textMessageModal]').within(() => {
cy.get('button')
.contains('Close')
.should('not.be.disabled')
.click();
});
cy.get('[data-testid=textMessageModal]').should('not.be.visible');
});
it.skip('shows received message by other user', () => {
//TODO: this should be test in e2e test
});
});
describe('coming from email links', () => {
it("opens details sidebar", () => {
cy.visit('/friends?open=details&id=1');
showSidePanelContent();
});
it("opens chat window", () => {
cy.visit('/friends?open=message&id=1');
cy.get('[data-testid=textMessageModal]')
.should('be.visible')
cy.contains('Send Message to Test User1').should('exist');
});
it("sends friend request", () => {
cy.intercept('GET', /\S+\/profile\S+/, { fixture: 'person' });
cy.intercept('POST', /\S+\/friend_requests/, { statusCode: 201, body: { ok: true } });
cy.visit('/friends?open=connect&id=1');
cy.get('[data-testid=profileSidePanel]')
.find('[data-testid=connect]')
.should('be.disabled');
cy.contains('Success! Your friend request has been sent to Test User1.');
});
})
describe('filter', () => {
const fillFilterForm = () => {
//cy.get('[data-testid=btnUpdateSearch]').click();
cy.get('[data-testid=modalUpdateSearch] input[name=latency_good]').uncheck();
cy.get('[data-testid=modalUpdateSearch] input[name=latency_fair]').uncheck();
cy.get('[data-testid=modalUpdateSearch] input[name=latency_high]').uncheck();
cy.get('[data-testid=modalUpdateSearch] input[name=proficiency_beginner]').uncheck();
cy.get('[data-testid=modalUpdateSearch] input[name=proficiency_intermediate]').uncheck();
cy.get('[data-testid=modalUpdateSearch] input[name=proficiency_expert]').uncheck();
cy.get('[data-testid=modalUpdateSearch] input[name=proficiency_expert]').uncheck();
cy.get('[data-testid=modalUpdateSearch] input[name=from_location]').uncheck();
cy.get('#selInstruments')
.type('Drums{enter}')
.click();
cy.get('#selGenres')
.type('Pop{enter}')
.click();
cy.get('#selLastActive').type('Within last 1 Day{enter}');
cy.get('#selJoinedWithin').type('Within last 7 Day{enter}');
};
beforeEach(() => {
cy.visit('/friends');
});
it('open and close filter modal', () => {
cy.get('[data-testid=btnUpdateSearch]').click();
cy.get('[data-testid=modalUpdateSearch]').should('be.visible');
cy.get('[data-testid=modalUpdateSearch]')
.contains('Cancel')
.click();
cy.get('[data-testid=modalUpdateSearch]').should('not.be.visible');
});
it('render filter form', () => {
cy.get('[data-testid=btnUpdateSearch]').click();
cy.get('[data-testid=modalUpdateSearch]').within(() => {
cy.get('input[name=latency_good]')
.should('be.checked')
.next()
.contains('Good (less than 40ms)');
cy.get('input[name=latency_fair]')
.should('be.checked')
.next()
.contains('Fair (40-60ms)');
cy.get('input[name=latency_high]')
.should('not.be.checked')
.next()
.contains('Poor (more than 60ms)');
cy.get('input[name=from_location]')
.should('not.be.checked')
});
});
it('reset filters', () => {
cy.get('[data-testid=btnUpdateSearch]').click();
fillFilterForm();
cy.get('[data-testid=modalUpdateSearch]')
.contains('Cancel')
.click();
cy.get('[data-testid=btnUpdateSearch]').click();
cy.get('[data-testid=modalUpdateSearch] input[name=latency_good]').should('not.be.checked');
cy.get('[data-testid=modalUpdateSearch]')
.contains('Cancel')
.click();
cy.get('[data-testid=btnResetSearch]').click(); //click reset button
cy.get('[data-testid=btnUpdateSearch]').click();
cy.get('[data-testid=modalUpdateSearch] input[name=latency_good]').should('be.checked');
});
it('submit filter form with params', () => {
//wait for stubbed request sent to fetch data for initial page load
cy.wait('@getPeople_page1').then(interception => {
assert.isNotNull(interception.response.body, '1st API call has data');
});
//the subsequent request sent to perfetch data and store in redux prefetched buffer
cy.wait('@getPeople_page2').then(interception => {
assert.isNotNull(interception.response.body, '1st API call has data - (prefethed)');
});
cy.get('[data-testid=btnUpdateSearch]').click();
cy.wait(1000);
cy.get('[data-testid=btnSubmitSearch]').click();
//wait for stubbed request sent by submitting search form without filling any form field
cy.wait('@getPeople_page1')
.then(interception => {
assert.isNotNull(interception.response.body, '3rd API call has data');
})
.its('request.body')
.should('deep.contain', {
latency_good: true,
latency_fair: true,
latency_high: false,
proficiency_beginner: true,
proficiency_intermediate: true,
proficiency_expert: true,
instruments: [],
genres: [],
from_location: false
});
cy.wait('@getPeople_page2').then(interception => {
assert.isNotNull(interception.response.body, '4th API call has data - (prefethed)');
});
cy.get('[data-testid=btnUpdateSearch]').click();
fillFilterForm(); // change filter options
cy.get('[data-testid=btnSubmitSearch]').click();
//wait for stubbed request sent by submitting search form again. but this time fill form fields
cy.wait('@getPeople_page1')
.then(interception => {
assert.isNotNull(interception.response.body, '5th API call has data');
})
.its('request.body')
.should('deep.contain', {
latency_good: false,
latency_fair: false,
latency_high: false,
proficiency_beginner: false,
proficiency_intermediate: false,
proficiency_expert: false,
instruments: [{ value: 'drums', label: 'Drums' }],
genres: ['pop'],
from_location: false
});
cy.wait('@getPeople_page2').then(interception => {
assert.isNotNull(interception.response.body, '6th API call has data - (prefethed)');
});
});
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,4 +5,8 @@
.even-row { .even-row {
border: 1px solid $gray-400; border: 1px solid $gray-400;
background-color: red; background-color: red;
}
.borderless td, .borderless th {
border: none;
} }

View File

@ -114,4 +114,13 @@
font-size: 8px; font-size: 8px;
position: relative; position: relative;
bottom: 2px; bottom: 2px;
} }
.person-row{
.offline{
color: #cac7c7;
&:hover{
color: #959090;
}
}
}

View File

@ -26,6 +26,7 @@ import JKHelp from '../page/JKHelp';
import JKPrivacy from '../page/JKPrivacy'; import JKPrivacy from '../page/JKPrivacy';
//import JKPeople from '../page/JKPeople'; //import JKPeople from '../page/JKPeople';
import JKPeopleFilter from '../page/JKPeopleFilter'; import JKPeopleFilter from '../page/JKPeopleFilter';
import JKMyFriends from '../page/JKMyFriends';
import JKNotifications from '../page/JKNotifications'; import JKNotifications from '../page/JKNotifications';
import JKMessageModal from '../profile/JKMessageModal'; import JKMessageModal from '../profile/JKMessageModal';
import JKUnsubscribe from '../page/JKUnsubscribe'; import JKUnsubscribe from '../page/JKUnsubscribe';
@ -255,6 +256,7 @@ function JKDashboardMain() {
<Route path="/privacy" component={JKPrivacy} /> <Route path="/privacy" component={JKPrivacy} />
<Route path="/help" component={JKHelp} /> <Route path="/help" component={JKHelp} />
<Route path="/unsubscribe" exact component={JKUnsubscribe} /> <Route path="/unsubscribe" exact component={JKUnsubscribe} />
<PrivateRoute path="/friends/my" component={JKMyFriends} />
<PrivateRoute path="/friends" component={JKPeopleFilter} /> <PrivateRoute path="/friends" component={JKPeopleFilter} />
<PrivateRoute path="/sessions/new" component={JKNewMusicSession} /> <PrivateRoute path="/sessions/new" component={JKNewMusicSession} />
<PrivateRoute path="/sessions/lobby" component={JKMusicSessionsLobby} /> <PrivateRoute path="/sessions/lobby" component={JKMusicSessionsLobby} />

View File

@ -76,8 +76,8 @@ function JKMusicSessionsLobby() {
useEffect(() => { useEffect(() => {
if (loadingStatus === 'succeeded' && onlineMusicians.length > 0) { if (loadingStatus === 'succeeded' && onlineMusicians.length > 0) {
const userIds = onlineMusicians.map(p => p.id); const otherUserIds = onlineMusicians.map(p => p.id);
const options = { currentUserId: currentUser.id, participantIds: userIds }; const options = { currentUserId: currentUser.id, otherUserIds };
dispatch(fetchUserLatencies(options)); dispatch(fetchUserLatencies(options));
} }
}, [loadingStatus]); }, [loadingStatus]);

View File

@ -0,0 +1,108 @@
import React, { useEffect, useRef, useState } from 'react';
import { Row, Col, Alert, Card, CardBody } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import { useResponsive } from '@farfetch/react-context-responsive';
import Loader from '../common/Loader';
import { isIterableArray } from '../../helpers/utils';
import JKFriendsList from '../people/JKFriendsList';
import JKFriendsSwiper from '../people/JKFriendsSwiper';
import { useAuth } from '../../context/UserAuth';
import { useDispatch, useSelector } from 'react-redux';
import { getFriends } from '../../helpers/rest';
import { filterFriendsByIds, sortFriends } from '../../store/features/friendsSlice';
import { fetchUserLatencies } from '../../store/features/latencySlice';
export const JKMyFriends = () => {
const { t } = useTranslation();
const { greaterThan, lessThan } = useResponsive();
const friendListRef = useRef();
const dispatch = useDispatch();
const [showLoadMore, setShowLoadMore] = useState(false);
const { currentUser } = useAuth();
const friends = useSelector(state => state.friend.friends);
const loadingStatus = useSelector(state => state.friend.loadingStatus);
const goNextPage = () => {};
const sortOptions = [
{ value: 'online', label: 'Online' },
{ value: 'alphabetical', label: 'Alphabetical' },
{ value: 'last_active', label: 'Last Active' }
];
useEffect(() => {
if (currentUser) {
getFriends(currentUser.id)
.then(async response => {
const data = await response.json();
const friendIds = data.map(friend => friend.id);
//fetch friends with associated data
dispatch(filterFriendsByIds({ userId: currentUser.id, ids: friendIds }));
//fetch latencies for friends
dispatch(fetchUserLatencies({ currentUserId: currentUser.id, otherUserIds: friendIds }));
})
.catch(error => {
console.error(error);
});
}
}, [currentUser]);
const sortResults = e => {
const sortValue = e.target.value;
dispatch(sortFriends(sortValue));
};
return (
<Card>
<FalconCardHeader title={t('page_title', { ns: 'friends' })} titleClass="font-weight-bold">
<div className={`d-flex ${ lessThan.sm? 'pt-2' : ''}`} >
<span className="mr-2">Sort By:</span>
<select onChange={sortResults}>
{sortOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</FalconCardHeader>
<CardBody className="pt-0">
<>
{loadingStatus === 'loading' && friends.length === 0 ? (
<Loader />
) : isIterableArray(friends) ? (
<>
{greaterThan.sm ? (
<Row className="mb-3 justify-content-between d-none d-md-block">
<div className="table-responsive-xl px-2" ref={friendListRef}>
<JKFriendsList friends={friends} isLoading={loadingStatus === 'loading'} />
</div>
</Row>
) : (
<Row className="swiper-container d-block d-md-none" data-testid="friendsSwiper">
<JKFriendsSwiper friends={friends} goNextPage={goNextPage} />
</Row>
)}
</>
) : (
loadingStatus === 'succeeded' &&
friends.length ===
0(
<Row className="p-card">
<Col>
<Alert color="info" className="mb-0">
No Records!
</Alert>
</Col>
</Row>
)
)}
</>
</CardBody>
</Card>
);
};
export default JKMyFriends;

View File

@ -1,147 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { Alert, Card, CardBody, Col, Row, Button, Form } from 'reactstrap';
import Loader from '../common/Loader';
import FalconCardHeader from '../common/FalconCardHeader';
import { isIterableArray } from '../../helpers/utils';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { fetchPeople } from '../../store/features/peopleSlice';
import JKPeopleSearch from './JKPeopleSearch';
import JKPeopleList from './JKPeopleList';
import JKPeopleSwiper from './JKPeopleSwiper';
import { useResponsive } from '@farfetch/react-context-responsive';
const JKPeople = ({ className }) => {
const [showSearch, setShowSearch] = useState(false);
const [page, setPage] = useState(1);
const [resetFilter, setResetFilter] = useState(false);
const peopleListRef = useRef();
const dispatch = useDispatch();
const { t } = useTranslation();
const people = useSelector(state => state.people.people);
const totalPages = useSelector(state => state.people.totalPages);
const loadingStatus = useSelector(state => state.people.status);
const { greaterThan } = useResponsive();
const loadPeople = React.useCallback(() => {
if (totalPages !== 0 && page > totalPages) {
setPage(totalPages + 1);
return;
}
try {
console.log('BEFORE fetching people');
dispatch(fetchPeople({ page }));
} catch (error) {
console.log('Error fetching people', error);
}
}, [page, totalPages, dispatch]);
useEffect(() => {
loadPeople();
}, [page]);
// useEffect(() => {
// if (loadingStatus === 'succeeded' && peopleListRef.current && page !== 1) {
// }
// }, [loadingStatus]);
const goNextPage = () => {
setPage(val => ++val);
};
const goPrevPage = () => {
if (page > 1) {
setPage(prev => --prev);
}
};
const handleScroll = () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
goNextPage();
}
};
useEffect(() => {
window.addEventListener('scroll', handleScroll, { passive: true });
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<Card>
<JKPeopleSearch
show={showSearch}
setShow={setShowSearch}
resetFilter={resetFilter}
setResetFilter={setResetFilter}
/>
<FalconCardHeader title={t('page_title', { ns: 'people' })} titleClass="font-weight-bold">
<Form inline className="mt-md-0 mt-3">
<Button
color="primary"
className="me-2 mr-2 fs--1"
onClick={() => setShowSearch(!showSearch)}
data-testid="btnUpdateSearch"
>
{t('update_search', { ns: 'people' })}
</Button>
<Button
outline
color="secondary"
className="fs--1"
data-testid="btnResetSearch"
onClick={() => setResetFilter(true)}
>
{t('reset_filters', { ns: 'people' })}
</Button>
</Form>
</FalconCardHeader>
<CardBody className="pt-0">
{loadingStatus === 'loading' && people.length === 0 ? (
<Loader />
) : isIterableArray(people) ? (
//Start Find Friends table hidden on small screens
<>
{greaterThan.xs ? (
<Row className="mb-3 justify-content-between d-none d-md-block">
<div className="table-responsive-xl px-2" ref={peopleListRef}>
<JKPeopleList people={people} />
{loadingStatus === 'loading' && people.length !== 0 && <span>loading...</span>}
</div>
</Row>
) : (
<Row className="swiper-container d-block d-md-none" data-testid="peopleSwiper">
<JKPeopleSwiper people={people} goNextPage={goNextPage} />
</Row>
)}
</>
) : (
<Row className="p-card">
<Col>
<Alert color="info" className="mb-0">
No Records!
</Alert>
</Col>
</Row>
)}
</CardBody>
</Card>
);
};
JKPeople.propTypes = {
className: PropTypes.string
};
JKPeople.defaultProps = {
className: 'col-6 col-md-4 col-lg-3 col-xxl-2'
};
export default JKPeople;

View File

@ -1,372 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import { Button, Card, CardBody, Form, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import Select from 'react-select';
import JKTooltip from '../common/JKTooltip';
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 JKPeople from './JKPeople';
function JKPeopleFilter() {
const { t } = useTranslation();
const [ show, setShow ] = useState(false);
const [resetFilter, setResetFilter] = useState(false);
const [instruments, setInstruments] = useState([]);
const [genres, setGenres] = useState([]);
const dispatch = useDispatch();
const pageToRequest = useRef(1)
const totalPages = useSelector(state => state.people.totalPages);
const { register, handleSubmit, setValue, control } = useForm({
defaultValues: {
latency_good: true,
latency_fair: true,
latency_high: false,
proficiency_beginner: true,
proficiency_intermediate: true,
proficiency_expert: true,
instruments: [],
genres: [],
joined_within_days: '-1',
active_within_days: '-1'
}
});
const { isDirty } = useFormState({ control });
const toggle = () => setShow(!show);
const fetchInstruments = async () => {
await getInstruments()
.then(response => {
if (response.ok) {
return response.json();
}
})
.then(data => {
setInstruments(
data.map(instrument => {
return {
value: instrument.id,
label: instrument.description
};
})
);
})
.catch(error => console.log(error));
};
const fetchGenres = async () => {
await getGenres()
.then(response => {
if (response.ok) {
return response.json();
}
})
.then(data => {
setGenres(
data.map(genre => {
return {
value: genre.id,
label: genre.description
};
})
);
})
.catch(error => {
console.log(error);
});
};
useEffect(() => {
if (resetFilter) {
clearFilterOpts();
setResetFilter(false);
dispatch(resetState());
pageToRequest.current = 1
handleSubmit(onSubmit)()
}
}, [resetFilter]);
const clearFilterOpts = () => {
setValue('latency_good', true)
setValue('latency_fair', true)
setValue('latency_high', false)
setValue('proficiency_beginner', true)
setValue('proficiency_intermediate', true)
setValue('proficiency_expert', true)
setValue('instruments', null)
setValue('genres', null)
setValue('joined_within_days', -1)
setValue('active_within_days', -1)
}
useEffect(() => {
fetchGenres();
fetchInstruments();
}, []);
const submitForm = event => {
event.preventDefault();
pageToRequest.current = 1
dispatch(resetState());
handleSubmit(onSubmit)(pageToRequest.current);
setShow(false);
};
const submitPageQuery = (page, hasNextPage) => {
handleSubmit(onSubmit)(page, hasNextPage)
}
const onSubmit = (data, page, hasNextPage = true) => {
let _page = pageToRequest.current > page ? pageToRequest.current : page
let genres = [];
let joined_within_days = '';
let active_within_days = '';
if (data.genres) {
genres = data.genres.map(genre => genre.value);
}
if(data.joined_within_days){
joined_within_days = data.joined_within_days.value;
}
if(data.active_within_days){
active_within_days = data.active_within_days.value;
}
const params = { ...data, genres, joined_within_days, active_within_days };
console.log("---DEBUG--- _page", _page);
console.log("---DEBUG--- totalPages", totalPages);
try {
dispatch(loadPrefetched())
if(totalPages && _page > totalPages){
return
}
dispatch(fetchPeople({ data: params, page: _page }));
pageToRequest.current++
if(hasNextPage){
dispatch(preFetchPeople({ data: params, page: _page + 1 }))
pageToRequest.current = pageToRequest.current + 2
}
} catch (error) {
console.log('error fetching people', error);
}
};
const lastActiveOpts = [
{ value: '', label: 'Any Range' },
{ value: '1', label: 'Within Last 1 Days' },
{ value: '7', label: 'Within Last 7 Days' },
{ value: '30', label: 'Within Last 30 Days' },
{ value: '90', label: 'Within Last 90 Days' }
];
const joinedOpts = [
{ value: '', label: 'Any Range' },
{ value: '1', label: 'Within Last 1 Days' },
{ value: '7', label: 'Within Last 7 Days' },
{ value: '30', label: 'Within Last 30 Days' },
{ value: '90', label: 'Within Last 90 Days' }
];
return (
<Card>
<FalconCardHeader title={t('page_title', { ns: 'people' })} titleClass="font-weight-bold">
<Form inline className="mt-md-0 mt-3">
<Button
color="primary"
className="me-2 mr-2 fs--1"
onClick={() => setShow(true)}
data-testid="btnUpdateSearch"
>
{t('update_search', { ns: 'people' })}
</Button>
<Button
outline
color="secondary"
className="fs--1"
data-testid="btnResetSearch"
onClick={() => setResetFilter(true) }
>
{t('reset_filters', { ns: 'people' })}
</Button>
</Form>
</FalconCardHeader>
<CardBody className="pt-0">
<Modal
isOpen={show}
toggle={toggle}
className="mw-100 mx-1 mr-1 ml-1 mx-md-5 mr-md-5 ml-md-5 mx-xl-10 mr-xl-10 ml-xl-10"
data-testid="modalUpdateSearch"
>
<ModalHeader toggle={toggle}>Update Search</ModalHeader>
<ModalBody>
<div className="px-4 pb-4">
<form>
<div className="row justify-content-start mt-2">
{/* first column */}
<div className="col-12 col-md-6 mb-3 mb-md-0">
<div className="row justify-content-start">
<div className="col-6">
<label className="form-label">
Latency{' '}
<JKTooltip title="Use these checkboxes to search for other musicians by the estimated amount of latency between you and them. Latency is the amount of time it takes for each of your computers to process audio, plus the time it takes for this digital audio to move over the Internet between you." />
</label>
<div className="form-check">
<input
{...register('latency_good')}
type="checkbox"
className="form-check-input"
defaultChecked={!isDirty}
onChange={e => setValue('latency_good', e.target.checked)}
/>
<label className="form-check-label">Good (less than 40ms)</label>
</div>
<div className="form-check">
<input
{...register('latency_fair')}
type="checkbox"
className="form-check-input"
defaultChecked={!isDirty}
onChange={e => setValue('latency_fair', e.target.checked)}
/>
<label className="form-check-label">Fair (40-60ms)</label>
</div>
<div className="form-check">
<input
{...register('latency_high')}
type="checkbox"
className="form-check-input"
onChange={e => setValue('latency_high', e.target.checked)}
/>
<label className="form-check-label">Poor (more than 60ms)</label>
</div>
</div>
<div className="col-6">
<label className="form-label">
Skill Level{' '}
<JKTooltip title="Use these checkboxes to search for other musicians by their skill level." />
</label>
<div className="form-check">
<input
{...register('proficiency_beginner')}
type="checkbox"
className="form-check-input"
defaultChecked={!isDirty}
onChange={e => setValue('proficiency_beginner', e.target.checked)}
/>
<label className="form-check-label">Beginner</label>
</div>
<div className="form-check">
<input
{...register('proficiency_intermediate')}
type="checkbox"
className="form-check-input"
defaultChecked={!isDirty}
onChange={e => setValue('proficiency_intermediate', e.target.checked)}
/>
<label className="form-check-label">Intermediate</label>
</div>
<div className="form-check">
<input
{...register('proficiency_expert')}
type="checkbox"
className="form-check-input"
defaultChecked={!isDirty}
onChange={e => setValue('proficiency_expert', e.target.checked)}
/>
<label className="form-check-label">Expert</label>
</div>
</div>
</div>
</div>
{/* second column */}
<div className="col-12 col-md-6">
<label className="form-label" htmlFor="instruments">
Instruments{' '}
<JKTooltip title="Use these checkboxes to search for other musicians who play particular instruments. If you do not select any instruments, we search for any/all instruments. If you select multiple instruments, we search for musicians who play any of these instruments." />
</label>
<div className="choices">
<Controller
name="instruments"
control={control}
render={({ field }) => (
<Select
{...field}
options={instruments}
isMulti
closeMenuOnSelect={false}
id="selInstruments"
/>
)}
/>
</div>
<label className="form-label" htmlFor="genres">
Genres{' '}
<JKTooltip title="Use these checkboxes to search for other musicians who enjoy playing particular musical genres/styles. If you do not select any genres, we search for any/all genres. If you select multiple genres, we search for musicians who play any of these genres." />
</label>
<div className="choices">
<Controller
name="genres"
control={control}
render={({ field }) => (
<Select {...field} options={genres} isMulti closeMenuOnSelect={false} id="selGenres" />
)}
/>
</div>
<label className="form-label" htmlFor="lastActive">
Last Active{' '}
<JKTooltip title="Use this list to search for other musicians who have been active on JamKazam within a specified time period. More recent activity makes it more likely they will respond if you message or request to connect." />
</label>
<div className="choices">
<Controller
name="active_within_days"
control={control}
render={({ field }) => <Select {...field} options={lastActiveOpts} id="selLastActive" />}
/>
</div>
<label className="form-label" htmlFor="joined">
Joined JamKazam{' '}
<JKTooltip title="Use this list to search for other musicians based on when they joined JamKazam. This can be useful for finding and connecting with newer users." />
</label>
<div className="choices">
<Controller
name="joined_within_days"
control={control}
render={({ field }) => <Select {...field} options={joinedOpts} id="selJoinedWithin" />}
/>
</div>
</div>
</div>
</form>
</div>
</ModalBody>
<ModalFooter>
<Button color="outline-primary" onClick={toggle}>
Cancel
</Button>{' '}
<Button color="primary" onClick={submitForm} data-testid="btnSubmitSearch">
Search
</Button>
</ModalFooter>
</Modal>
<JKPeople onPageChange={submitPageQuery} />
</CardBody>
</Card>
);
}
export default JKPeopleFilter;

View File

@ -1,38 +0,0 @@
import React from 'react';
import { Table } from 'reactstrap';
import JKPerson from './JKPerson';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
const JKPeopleList = ({ people }) => {
const { t } = useTranslation();
return (
<Table striped bordered className="fs--1" data-testid="peopleListTable">
<thead className="bg-200 text-900">
<tr>
<th scope="col">{t('person_attributes.name', { ns: 'people' })}</th>
<th scope="col" style={{ minWidth: 250 }}>
{t('person_attributes.about', { ns: 'people' })}
</th>
<th scope="col">{t('person_attributes.instruments', { ns: 'people' })}</th>
<th scope="col">{t('person_attributes.genres', { ns: 'people' })}</th>
<th scope="col">{t('actions', { ns: 'common' })}</th>
</tr>
</thead>
<tbody className="list">
{people.map((person) => (
// <tr className="align-middle" key={`people-list-item-${person.id}`}>
<JKPerson person={person} viewMode="list" key={`jk-person-${person.id}`} />
// </tr>
))}
</tbody>
</Table>
);
};
JKPeopleList.propTypes = {
people: PropTypes.arrayOf(PropTypes.instanceOf(Object))
};
export default JKPeopleList;

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Row, Col } from 'reactstrap'; import { Row, Col } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@ -20,13 +20,16 @@ import JKLatencyBadge from '../profile/JKLatencyBadge';
import JKLastActiveAgo from '../profile/JKLastActiveAgo'; import JKLastActiveAgo from '../profile/JKLastActiveAgo';
const JKPerson = props => { const JKPerson = props => {
const { id, name, biography, photo_url, instruments, genres, latency_data, last_active_timestamp } = props.person; const { id, name, biography, photo_url, instruments, genres, latency_data, last_active_timestamp, online } = props.person;
const { currentUser } = useAuth(); const { currentUser } = useAuth();
const [showSidePanel, setShowSidePanel] = useState(false); const [showSidePanel, setShowSidePanel] = useState(false);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const user = useSelector(state => state.people.people.find(p => p.id === id)); const user = useSelector(state => state.people.people.find(p => p.id === id));
const { greaterThan } = useResponsive() const latencyData = useSelector(state => state.latency.latencies.find(l => l.user_id === id));
const { greaterThan } = useResponsive();
console.log('latency_data', latency_data);
console.log('latencyData', latencyData);
const toggleMoreDetails = async e => { const toggleMoreDetails = async e => {
if(e) if(e)
@ -51,14 +54,23 @@ const JKPerson = props => {
toggleMoreDetails() toggleMoreDetails()
} }
} }
}, []) }, [])
const linkClass = useMemo(() => {
if (!online) {
return `d-flex align-items-center mb-1 fs-0 person-link offline`;
}
return `d-flex align-items-center mb-1 fs-0 person-link`;
}, [online]);
return ( return (
<> <>
{greaterThan.sm ? ( {greaterThan.sm ? (
<tr className="align-middle" key={`people-list-item-${id}`}> <tr className="align-middle person-row" key={`people-list-item-${id}`}>
<td className="text-nowrap"> <td className="text-nowrap">
<a href="/#" onClick={toggleMoreDetails} className="d-flex align-items-center mb-1 fs-0 person-link"> <a href="/#" onClick={toggleMoreDetails} className={linkClass}>
<div className="avatar avatar-xl"> <div className="avatar avatar-xl">
<JKProfileAvatar url={photo_url} /> <JKProfileAvatar url={photo_url} />
</div> </div>
@ -67,11 +79,15 @@ const JKPerson = props => {
</div> </div>
</a> </a>
<div> <div>
<strong>{t('person_attributes.latency_to_me', { ns: 'people' })}:</strong> <strong className='mr-1'>{t('person_attributes.status', { ns: 'people' })}:</strong>
<JKLatencyBadge latencyData={latency_data} /> <span>{ online ? 'Online' : 'Offline'}</span>
</div> </div>
<div> <div>
<strong>{t('person_attributes.last_active', { ns: 'people' })}:</strong>{' '} <strong className='mr-1'>{t('person_attributes.latency_to_me', { ns: 'people' })}:</strong>
<JKLatencyBadge latencyData={latency_data || latencyData } />
</div>
<div>
<strong className='mr-1'>{t('person_attributes.last_active', { ns: 'people' })}:</strong>{' '}
<JKLastActiveAgo timestamp={last_active_timestamp} /> <JKLastActiveAgo timestamp={last_active_timestamp} />
</div> </div>
</td> </td>
@ -118,11 +134,15 @@ const JKPerson = props => {
<Row> <Row>
<Col> <Col>
<div> <div>
<strong>{t('person_attributes.latency_to_me', { ns: 'people' })}:</strong>{' '} <strong className='mr-1'>{t('person_attributes.status', { ns: 'people' })}:</strong>
<span>{ online ? 'Online' : 'Offline'}</span>
</div>
<div>
<strong className='mr-1'>{t('person_attributes.latency_to_me', { ns: 'people' })}:</strong>{' '}
<JKLatencyBadge latencyData={latency_data} /> <JKLatencyBadge latencyData={latency_data} />
</div> </div>
<div> <div>
<strong>{t('person_attributes.last_active', { ns: 'people' })}:</strong>{' '} <strong className='mr-1'>{t('person_attributes.last_active', { ns: 'people' })}:</strong>{' '}
<JKLastActiveAgo timestamp={last_active_timestamp} /> <JKLastActiveAgo timestamp={last_active_timestamp} />
</div> </div>
</Col> </Col>

View File

@ -0,0 +1,44 @@
import React from 'react';
import { Table, Button } from 'reactstrap';
import JKPerson from '../page/JKPerson';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
const JKFriendsList = ({ friends, isLoading }) => {
const { t } = useTranslation();
return (
<>
<Table striped bordered className="fs--1" data-testid="peopleListTable">
<thead className="bg-200 text-900">
<tr>
<th scope="col">{t('person_attributes.name', { ns: 'people' })}</th>
<th scope="col" style={{ minWidth: 250 }}>
{t('person_attributes.about', { ns: 'people' })}
</th>
<th scope="col">{t('person_attributes.instruments', { ns: 'people' })}</th>
<th scope="col">{t('person_attributes.genres', { ns: 'people' })}</th>
<th scope="col">{t('actions', { ns: 'common' })}</th>
</tr>
</thead>
<tbody className="list">
{friends.map(friend => (
<JKPerson person={friend} viewMode="list" key={`jk-person-${friend.id}`} />
))}
</tbody>
</Table>
{/* {hasNext && (
<Button color="primary" outline={true} onClick={() => goNextPage()} disabled={isLoading} data-testid="paginate-next-page">
{isLoading ? <span>Loading...</span> : <span>Load More</span>}
</Button>
)} */}
</>
);
};
JKFriendsList.propTypes = {
friends: PropTypes.arrayOf(PropTypes.instanceOf(Object)),
isLoading: PropTypes.bool.isRequired
};
export default JKFriendsList;

View File

@ -0,0 +1,75 @@
import React from 'react';
import PropTypes from 'prop-types';
// import Swiper core and required modules
import SwiperCore, { Navigation, Pagination, Scrollbar, A11y } from 'swiper';
// Import Swiper React components
import { Swiper, SwiperSlide } from 'swiper/react';
// Import Swiper styles
import 'swiper/swiper.scss';
import 'swiper/components/navigation/navigation.scss';
import 'swiper/components/pagination/pagination.scss';
import 'swiper/components/scrollbar/scrollbar.scss';
import { Card, CardBody, CardHeader } from 'reactstrap';
import JKPerson from '../page/JKPerson';
import JKProfileAvatar from '../profile/JKProfileAvatar';
SwiperCore.use([Navigation, Pagination, Scrollbar, A11y]);
const JKFriendsSwiper = ({ friends, goNextPage }) => {
return (
<>
<Swiper
spaceBetween={0}
slidesPerView={1}
onSlideChange={() => console.log('slide change')}
onSlideNextTransitionEnd={swiper => {
if(swiper.isEnd){
goNextPage()
}
}}
pagination={{
clickable: true,
type: 'custom'
}}
navigation={{
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}}
>
{friends.map((friend, index) => (
<SwiperSlide key={`friends-swiper-item-${friend.id}`}>
<Card className="swiper-person-card">
<CardHeader className="bg-200">
<div className="avatar avatar-xl d-inline-block me-2 mr-2">
<JKProfileAvatar url={friend.photo_url} size="xl"/>
</div>
<h5 className="d-inline-block align-top mt-1">{friend.name}</h5>
</CardHeader>
<CardBody>
<JKPerson person={friend} viewMode="swipe" />
</CardBody>
</Card>
</SwiperSlide>
))}
</Swiper>
<div className="py-4 px-6 bg-white border-top w-100 fixed-bottom">
<div className="swiper-pagination" />
<div className="swiper-button-prev" />
<div className="swiper-button-next" />
</div>
</>
);
};
JKFriendsSwiper.propTypes = {
friends: PropTypes.arrayOf(PropTypes.instanceOf(Object)),
goNextPage: PropTypes.func
};
export default JKFriendsSwiper;

View File

@ -12,7 +12,7 @@ const JKConnectButton = props => {
const [showConfirmModal, setShowConfirmModal] = useState(false); const [showConfirmModal, setShowConfirmModal] = useState(false);
useEffect(() => { useEffect(() => {
if(user){ if (user) {
setIsFriend(user.is_friend); setIsFriend(user.is_friend);
setPendingFriendRequest(user.pending_friend_request); setPendingFriendRequest(user.pending_friend_request);
} }
@ -22,16 +22,15 @@ const JKConnectButton = props => {
useEffect(() => { useEffect(() => {
const openWin = queryString.get('open'); const openWin = queryString.get('open');
const userId = queryString.get('id') const userId = queryString.get('id');
//sending friend request if directly reqested to do so //sending friend request if directly reqested to do so
//by query string params (coming from weekly new user match email link) //by query string params (coming from weekly new user match email link)
if(openWin && userId && userId === user.id && !user.isFriend && !user.pending_friend_request){ if (openWin && userId && userId === user.id && !user.isFriend && !user.pending_friend_request) {
if(openWin === 'connect'){ if (openWin === 'connect') {
addFriend(); addFriend();
} }
} }
}, []) }, []);
const addFriend = () => { const addFriend = () => {
setShowConfirmModal(!showConfirmModal); setShowConfirmModal(!showConfirmModal);
@ -71,7 +70,7 @@ const JKConnectButton = props => {
const buttonTitle = () => { const buttonTitle = () => {
let title; let title;
if (pendingFriendRequest) { if (pendingFriendRequest) {
title = 'There is a pending friend request'; title = 'Delete pending friend request';
} else if (!isFriend) { } else if (!isFriend) {
title = 'Send friend request'; title = 'Send friend request';
} else if (isFriend) { } else if (isFriend) {
@ -101,14 +100,22 @@ const JKConnectButton = props => {
/> */} /> */}
</> </>
) : ( ) : (
<button <>
className={`btn btn-primary ${cssClasses}`} <button
data-testid="disconnect" className={`btn btn-primary ${cssClasses}`}
onClick={removeFriend} data-testid="disconnect"
title={buttonTitle()} onClick={() => setShowConfirmModal(!showConfirmModal)}
> title={buttonTitle()}
{removeContent} >
</button> {removeContent}
</button>
<RemoveConfirmModal
show={showConfirmModal}
setShow={setShowConfirmModal}
user={user}
handleOnConfirm={removeFriend}
/>
</>
)} )}
</> </>
); );
@ -133,7 +140,7 @@ JKConnectButton.propTypes = {
// }; // };
// return ( // return (
// <Modal isOpen={show} toggle={toggle} className={className} centered={true} data-testid="confirmFriendRequestModal"> // <Modal isOpen={show} toggle={toggle} className={className} centered={true} data-testid="confirmFriendRequestModal">
// <ModalHeader toggle={toggle}>Send Friend Request</ModalHeader> // <ModalHeader toggle={toggle}>Send Friend Request</ModalHeader>
// <ModalBody>Send a friend request to {user.name}.</ModalBody> // <ModalBody>Send a friend request to {user.name}.</ModalBody>
@ -146,8 +153,33 @@ JKConnectButton.propTypes = {
// </Button> // </Button>
// </ModalFooter> // </ModalFooter>
// </Modal> // </Modal>
// ); // );
// }; // };
const RemoveConfirmModal = props => {
const { className, show, setShow, user, handleOnConfirm } = props;
const toggle = () => setShow(!show);
const accept = () => {
handleOnConfirm();
};
return (
<Modal isOpen={show} toggle={toggle} className={className} centered={true} data-testid="confirmRemoveFriendModal">
<ModalHeader toggle={toggle}>Remove Friend</ModalHeader>
<ModalBody>Are you sure to remove {user.name} from your friend list?</ModalBody>
<ModalFooter>
<Button color="primary" onClick={accept}>
Yes
</Button>{' '}
<Button color="secondary" onClick={toggle}>
Cancel
</Button>
</ModalFooter>
</Modal>
);
};
export default JKConnectButton; export default JKConnectButton;

View File

@ -25,8 +25,8 @@ function JKSession({ session }) {
const { setNativeAppUnavailable } = useNativeApp(); const { setNativeAppUnavailable } = useNativeApp();
useEffect(() => { useEffect(() => {
const participantIds = session.participants.map(p => p.user.id); const otherUserIds = session.participants.map(p => p.user.id);
const options = { currentUserId: currentUser.id, participantIds }; const options = { currentUserId: currentUser.id, otherUserIds };
dispatch(fetchUserLatencies(options)); dispatch(fetchUserLatencies(options));
}, [session.id]); }, [session.id]);

View File

@ -45,6 +45,17 @@ export const getPeople = ({ data, offset, limit } = {}) => {
}); });
}; };
export const getPeopleByIds = ({ userId, ids }) => {
return new Promise((resolve, reject) => {
apiFetch(`/users/${userId}/filter_by_ids`, {
method: 'POST',
body: JSON.stringify({ user_ids: ids })
})
.then(response => resolve(response))
.catch(error => reject(error));
});
};
export const getPeopleIndex = () => { export const getPeopleIndex = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
apiFetch(`/users`) apiFetch(`/users`)

View File

@ -4,6 +4,7 @@ import { initReactI18next } from "react-i18next";
import commonTranslationsEN from './locales/en/common.json' import commonTranslationsEN from './locales/en/common.json'
import homeTranslationsEN from './locales/en/home.json' import homeTranslationsEN from './locales/en/home.json'
import peopleTranslationsEN from './locales/en/people.json' import peopleTranslationsEN from './locales/en/people.json'
import friendsTranslationsEN from './locales/en/friends.json'
import authTranslationsEN from './locales/en/auth.json' import authTranslationsEN from './locales/en/auth.json'
import sessTranslationsEN from './locales/en/sessions.json' import sessTranslationsEN from './locales/en/sessions.json'
import unsubscribeTranslationsEN from './locales/en/unsubscribe.json' import unsubscribeTranslationsEN from './locales/en/unsubscribe.json'
@ -13,6 +14,7 @@ import accountEN from './locales/en/account.json'
import commonTranslationsES from './locales/es/common.json' import commonTranslationsES from './locales/es/common.json'
import homeTranslationsES from './locales/es/home.json' import homeTranslationsES from './locales/es/home.json'
import peopleTranslationsES from './locales/es/people.json' import peopleTranslationsES from './locales/es/people.json'
import friendsTranslationsES from './locales/es/friends.json'
import authTranslationsES from './locales/es/auth.json' import authTranslationsES from './locales/es/auth.json'
import sessTranslationsES from './locales/es/sessions.json' import sessTranslationsES from './locales/es/sessions.json'
import unsubscribeTranslationsES from './locales/es/unsubscribe.json' import unsubscribeTranslationsES from './locales/es/unsubscribe.json'
@ -32,7 +34,8 @@ i18n.use(initReactI18next).init({
sessions: sessTranslationsEN, sessions: sessTranslationsEN,
unsubscribe: unsubscribeTranslationsEN, unsubscribe: unsubscribeTranslationsEN,
profile: profileEN, profile: profileEN,
account: accountEN account: accountEN,
friends: friendsTranslationsEN
}, },
es: { es: {
//translations: require('./locales/es/translations.json') //translations: require('./locales/es/translations.json')
@ -43,7 +46,8 @@ i18n.use(initReactI18next).init({
sessions: sessTranslationsES, sessions: sessTranslationsES,
unsubscribe: unsubscribeTranslationsES, unsubscribe: unsubscribeTranslationsES,
profile: profileES, profile: profileES,
account: accountES account: accountES,
friends: friendsTranslationsES
} }
}, },
//ns: ['translations'], //ns: ['translations'],

View File

@ -0,0 +1,3 @@
{
"page_title": "My Friends"
}

View File

@ -19,6 +19,7 @@
"bands": "Bands", "bands": "Bands",
"performance_samples": "Performance Samples", "performance_samples": "Performance Samples",
"online_presence": "Online Presence", "online_presence": "Online Presence",
"interests": "Interests" "interests": "Interests",
"status": "Status"
} }
} }

View File

@ -0,0 +1,3 @@
{
"page_title": "Mis amigos"
}

View File

@ -13,8 +13,8 @@ export const friendsRoute = {
exact: true, exact: true,
icon: 'users', icon: 'users',
children: [ children: [
{ to: '/friends', name: 'My Friends' }, { to: '/friends/my', name: 'My Friends' },
{ to: '/friends/find', name: 'Find Friends' } { to: '/friends', name: 'Find Friends' }
] ]
} }

View File

@ -0,0 +1,65 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { getFriends, getPeopleByIds } from '../../helpers/rest'
const initialState = {
friends: [],
status: 'idel',
error: null,
}
export const fetchFriends = createAsyncThunk(
"friend/fetchFriends",
async (options, thunkAPI) => {
const { userId } = options
const response = await getFriends(userId);
return response.json();
}
)
export const filterFriendsByIds = createAsyncThunk(
"friend/filterFriendsByIds",
async (options, thunkAPI) => {
const { userId, ids } = options;
const response = await getPeopleByIds({ userId, ids });
return response.json();
}
)
export const FriendSlice = createSlice({
name: "friend",
initialState,
reducers: {
addFriend: (state) => {},
updateFriend: (state) => {},
deleteFriend: (state) => {},
sortFriends: (state, action) => {
console.log('sorting friends', action.payload)
const sortValue = action.payload;
if(sortValue === 'online'){
state.friends = state.friends.sort((a, b) => a.online - b.online).sort((a, b) => a.name.localeCompare(b.name));
} else if(sortValue === 'alphabetical'){
state.friends = state.friends.sort((a, b) => a.name.localeCompare(b.name));
} else if(sortValue === 'last_active'){
state.friends = state.friends.sort((a, b) => b.last_active_timestamp - a.last_active_timestamp);
}
},
},
extraReducers: (builder) => {
builder
.addCase(filterFriendsByIds.pending, (state, action) => {
state.status = "loading";
})
.addCase(filterFriendsByIds.fulfilled, (state, action) => {
state.status = "succeeded";
state.friends = action.payload.musicians.sort((a, b) => a.online - b.online).sort((a, b) => a.name.localeCompare(b.name));
})
.addCase(filterFriendsByIds.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
}
})
export const { sortFriends } = FriendSlice.actions;
export default FriendSlice.reducer;

View File

@ -10,8 +10,8 @@ const initialState = {
export const fetchUserLatencies = createAsyncThunk( export const fetchUserLatencies = createAsyncThunk(
'latency/fetchUserLatencies', 'latency/fetchUserLatencies',
async (options, thunkAPI) => { async (options, thunkAPI) => {
const { currentUserId, participantIds } = options const { currentUserId, otherUserIds } = options
const response = await getLatencyToUsers(currentUserId, participantIds) const response = await getLatencyToUsers(currentUserId, otherUserIds)
return response.json() return response.json()
} }
) )
@ -37,7 +37,6 @@ export const latencySlice = createSlice({
state.status = 'succeeded' state.status = 'succeeded'
}) })
.addCase(fetchUserLatencies.rejected, (state, action) => { .addCase(fetchUserLatencies.rejected, (state, action) => {
console.log("_DEBUG_ fail fetchUserLatencies", action.payload);
state.status = 'failed' state.status = 'failed'
}) })
} }

View File

@ -6,6 +6,7 @@ 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'
import latencyReducer from "./features/latencySlice" import latencyReducer from "./features/latencySlice"
import friendReducer from "./features/friendsSlice"
export default configureStore({ export default configureStore({
reducer: { reducer: {
@ -15,6 +16,7 @@ export default configureStore({
session: sessionReducer, session: sessionReducer,
latency: latencyReducer, latency: latencyReducer,
onlineMusician: onlineMusicianReducer, onlineMusician: onlineMusicianReducer,
lobbyChat: lobbyChatMessagesReducer lobbyChat: lobbyChatMessagesReducer,
friend: friendReducer
} }
}) })

View File

@ -31,6 +31,15 @@ class ApiUsersController < ApiController
respond_with @users, responder: ApiResponder, :status => 200 respond_with @users, responder: ApiResponder, :status => 200
end end
def filter_by_ids
user = User.find(params[:id])
user_ids = []
user_ids = params[:user_ids].split(',') if params[:user_ids]
sobj = JamRuby::MusicianSearch.user_search_filter(user)
@search = sobj.user_search_results(user_ids)
respond_with @search, responder: ApiResponder, status: 201, template: 'api_search/filter'
end
def calendar def calendar
#@user=lookup_user #@user=lookup_user
#ics = CalendarManager.new.create_ics_feed(@user) #ics = CalendarManager.new.create_ics_feed(@user)

View File

@ -2,7 +2,7 @@ object @search
node :offset do node :offset do
@nextOffset @nextOffset
end end if @nextOffset
# node :page_count do |foo| # node :page_count do |foo|
# @search.page_count # @search.page_count

View File

@ -543,6 +543,8 @@ Rails.application.routes.draw do
match '/users/:id/broadcast_notification' => 'api_users#broadcast_notification', :via => :get match '/users/:id/broadcast_notification' => 'api_users#broadcast_notification', :via => :get
match '/users/:id/broadcast_notification/:broadcast_id/quiet' => 'api_users#quiet_broadcast_notification', :via => :post match '/users/:id/broadcast_notification/:broadcast_id/quiet' => 'api_users#quiet_broadcast_notification', :via => :post
match '/users/:id/filter_by_ids' => 'api_users#filter_by_ids', :via => [:post]
# session chat # session chat
match '/chat' => 'api_chats#create', :via => :post match '/chat' => 'api_chats#create', :via => :post
match '/chat' => 'api_chats#index', :via => :get match '/chat' => 'api_chats#index', :via => :get