diff --git a/jam-ui/.nvmrc b/jam-ui/.nvmrc index 2ccd0b76b..4a9c19cb5 100644 --- a/jam-ui/.nvmrc +++ b/jam-ui/.nvmrc @@ -1 +1 @@ -v14.17.1 +v14.21.3 diff --git a/jam-ui/src/components/common/JKModalDialog.js b/jam-ui/src/components/common/JKModalDialog.js index 307ce5221..16c0377ca 100644 --- a/jam-ui/src/components/common/JKModalDialog.js +++ b/jam-ui/src/components/common/JKModalDialog.js @@ -3,7 +3,7 @@ import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; import { useTranslation } from 'react-i18next'; import PropTypes from 'prop-types'; -const JKModalDialog = ({ title, children, show, onToggle }) => { +const JKModalDialog = ({ title, children, show, onToggle, showFooter }) => { const [modal, setModal] = useState(show); const toggle = () => { @@ -21,9 +21,11 @@ const JKModalDialog = ({ title, children, show, onToggle }) => { {title} {children} - - - + {showFooter && ( + + + + )} ); }; @@ -32,12 +34,14 @@ JKModalDialog.propTypes = { show: PropTypes.bool.isRequired, title: PropTypes.string.isRequired, children: PropTypes.node.isRequired, - onToggle: PropTypes.func.isRequired + onToggle: PropTypes.func.isRequired, + showFooter: PropTypes.bool }; JKModalDialog.defaultProps = { show: false, - title: 'Modal Dialog' + title: 'Modal Dialog', + showFooter: true }; export default JKModalDialog; diff --git a/jam-ui/src/components/page/JKMusicSessionsLobby.js b/jam-ui/src/components/page/JKMusicSessionsLobby.js index 57bb9b5c2..ceff267a1 100644 --- a/jam-ui/src/components/page/JKMusicSessionsLobby.js +++ b/jam-ui/src/components/page/JKMusicSessionsLobby.js @@ -1,26 +1,54 @@ -import React, { useEffect } from 'react'; -import { Col, Row, Card, CardBody } from 'reactstrap'; -import FalconCardHeader from '../common/FalconCardHeader'; +import React, { useEffect, useState } from 'react'; +import { Col, Row, Card, CardBody, Button, CardHeader, Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap'; import { useTranslation } from 'react-i18next'; import { useResponsive } from '@farfetch/react-context-responsive'; import JKLobbyUserList from '../sessions/JKLobbyUserList'; +import JKLobbyUserSwiper from '../sessions/JKLobbyUserSwiper'; import JKLobbyChat from '../sessions/JKLobbyChat'; import { useDispatch, useSelector } from 'react-redux'; import { fetchOnlineMusicians } from '../../store/features/onlineMusiciansSlice'; import { fetchUserLatencies } from '../../store/features/latencySlice'; import { useAuth } from '../../context/UserAuth'; +import { sessionPrivacyMap } from '../../config'; +import jkCustomUrlScheme from '../../helpers/jkCustomUrlScheme'; +import useNativeAppCheck from '../../hooks/useNativeAppCheck'; +import { useNativeApp } from '../../context/NativeAppContext'; +import JKModalDialog from '../common/JKModalDialog'; +import JKTooltip from '../common/JKTooltip'; function JKMusicSessionsLobby() { const { t } = useTranslation(); const { greaterThan } = useResponsive(); const dispatch = useDispatch(); const { currentUser } = useAuth(); + const isNativeAppAvailable = useNativeAppCheck(); + const { nativeAppUnavailable, setNativeAppUnavailable } = useNativeApp(); const onlineMusicians = useSelector(state => state.onlineMusician.musicians); const loadingStatus = useSelector(state => state.onlineMusician.status); + const [selectedUsers, setSelectedUsers] = useState([]); + const [submitted, setSubmitted] = useState(false); + const [activeTab, setActiveTab] = useState('1'); + const [showNotificationsModal, setShowNotificationsModal] = useState(false); + useEffect(() => { dispatch(fetchOnlineMusicians()); + + //check if browser notifications are enabled + try { + const notificationsEnabled = localStorage.getItem('showLobbyChatNotifications'); + const dontAskAgain = localStorage.getItem('dontAskLobbyChatNotificationPermission'); + if (notificationsEnabled || dontAskAgain) { + return; + } else { + setTimeout(() => { + if (true) { + setShowNotificationsModal(true); + } + }, 10000); + } + } catch (error) {} }, []); useEffect(() => { @@ -31,30 +59,216 @@ function JKMusicSessionsLobby() { } }, [loadingStatus]); + const handleClick = async () => { + const payload = { + privacy: sessionPrivacyMap.public, + description: t('list.descriptions.public_open_session', { ns: 'sessions' }), + inviteeIds: selectedUsers + }; + try { + //throw new Error('test'); + await isNativeAppAvailable(); + //window.open jamkazam app url using custom URL scheme + //an example URL would be: jamkazam://url=https://www.jamkazam.com/client#/createSession/privacy~2|description~hello|inviteeIds~1,2,3,4 + const q = `privacy~${payload.privacy}|description~${payload.description}|inviteeIds~${payload.inviteeIds}`; + const urlScheme = jkCustomUrlScheme('createSession', q); + setSubmitted(true); + window.open(urlScheme); + //history.push('/sessions'); + } catch (error) { + toggleAppUnavilableModel(); + } finally { + setSubmitted(false); + } + return false; + }; + + const toggleAppUnavilableModel = () => { + setNativeAppUnavailable(prev => !prev); + if (!nativeAppUnavailable) { + setSubmitted(false); + } + }; + + const toggleShowNotificationsModel = () => { + setShowNotificationsModal(prev => !prev); + }; + + const grantShowNotificationsPermission = () => { + setShowNotificationsModal(false); + try { + if (!window.Notification) { + console.log('Your web browser does not support notifications.'); + alert('Your web browser does not support notifications.'); + } else { + // check if permission is already granted + if (Notification.permission === 'granted') { + // show notification here + } else { + // request permission from user + Notification.requestPermission() + .then(function(p) { + if (p === 'granted') { + new Notification('Lobby Chat Notifications', { + body: 'Notifications will appear here.', + }); + localStorage.setItem('showLobbyChatNotifications', true); + } else { + console.log('User blocked notifications.'); + } + }) + .catch(function(err) { + console.error(err); + }); + } + } + } catch (error) {} + }; + + const handleDontAsk = e => { + const checked = e.target.checked; + if (!checked) { + return; + } + try { + localStorage.setItem('dontAskLobbyChatNotificationPermission', true); + } catch (error) {} + }; + return ( -
- <> - - - - {greaterThan.sm ? ( - - -
- -
- - - - -
- ) : ( - + <> + + +
+
+
{t('lobby.page_title', { ns: 'sessions' })}
+ +
+ {greaterThan.sm && ( +
+ +
)} - - - -
+
+ + + {greaterThan.sm ? ( + + +
+ +
+ + + + +
+ ) : ( + + + + + + + + + + + + + + + + + + + + + )} +
+ + +

{t('modals.native_app_unavailable.body', { ns: 'common' })}

+
+ toggleAppUnavilableModel()} + target="_blank" + className="btn btn-primary mr-2" + > + {t('modals.native_app_unavailable.download_button', { ns: 'common' })} + + toggleAppUnavilableModel()} + target="_blank" + className="btn btn-light" + > + {t('modals.native_app_unavailable.help_button', { ns: 'common' })} + +
+
+ + +

{t('lobby.chat_notifications.body', { ns: 'sessions' })}

+
+
+ +
+
+ + +
+
+
+ ); } diff --git a/jam-ui/src/components/page/JKNewMusicSession.js b/jam-ui/src/components/page/JKNewMusicSession.js index 4eeab5825..509bc48bb 100644 --- a/jam-ui/src/components/page/JKNewMusicSession.js +++ b/jam-ui/src/components/page/JKNewMusicSession.js @@ -13,12 +13,13 @@ import JKModalDialog from '../common/JKModalDialog'; import useNativeAppCheck from '../../hooks/useNativeAppCheck'; import { useNativeApp } from '../../context/NativeAppContext'; import { useResponsive } from '@farfetch/react-context-responsive'; +import { sessionPrivacyMap } from '../../config'; +// const privacyMap = { +// public: 1, +// private_invite: 2, +// private_approve: 3 +// }; -const privacyMap = { - public: 1, - private_invite: 2, - private_approve: 3 -}; const JKNewMusicSession = () => { const { currentUser } = useAuth(); @@ -54,7 +55,6 @@ const JKNewMusicSession = () => { } }) .then(data => { - console.log('friends = ', data); setFriends(data); setIsFriendsFetched(true); }); @@ -85,7 +85,6 @@ const JKNewMusicSession = () => { description: formData.get('description'), inviteeIds: invitees.map(i => i.id).join() }; - console.log(payload); try { //store this payload in localstorage. localStorage.setItem('formData', JSON.stringify(payload)); @@ -155,11 +154,11 @@ const JKNewMusicSession = () => { onChange={e => setPrivacy(e.target.value)} data-testid="session-privacy" > - - + - diff --git a/jam-ui/src/components/sessions/JKLobbyChat.js b/jam-ui/src/components/sessions/JKLobbyChat.js index 95535dcf7..0fdc354a7 100644 --- a/jam-ui/src/components/sessions/JKLobbyChat.js +++ b/jam-ui/src/components/sessions/JKLobbyChat.js @@ -6,6 +6,8 @@ import { useAuth } from '../../context/UserAuth'; import JKProfileAvatar from '../profile/JKProfileAvatar'; import TimeAgo from '../common/JKTimeAgo'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import { useResponsive } from '@farfetch/react-context-responsive'; import useOnScreen from '../../hooks/useOnScreen'; import useKeepScrollPosition from '../../hooks/useKeepScrollPosition'; @@ -17,6 +19,7 @@ function JKLobbyChat() { const dispatch = useDispatch(); const messageTextBox = useRef(); const scrollbar = useRef(); + const { greaterThan } = useResponsive(); const scrolledToBottom = useRef(false); const { currentUser } = useAuth(); const [fetching, setFetching] = useState(false); @@ -126,7 +129,9 @@ function JKLobbyChat() { useEffect(() => { if (createStatus === 'succeeded') { - fetchMessages({ start: 0, limit: 1, lastOnly: true }); + // fetchMessages({ start: 0, limit: 1, lastOnly: true }); + setMessages([]) + fetchMessages({ start: 0, limit: offset === 0 ? LIMIT : offset * LIMIT}) messageTextBox.current.focus(); } }, [createStatus]); @@ -152,12 +157,12 @@ function JKLobbyChat() {
{messages.map((message, i) => ( -
-
(i === 0 ? setLastMessageRef(ref) : null)}> -
+
+
(i === 0 ? setLastMessageRef(ref) : null)}> +
-
+
{message.user.name} @@ -171,9 +176,9 @@ function JKLobbyChat() { )}
- { message.id } {message.message} - + {/*
+ {message.id} */}
diff --git a/jam-ui/src/components/sessions/JKLobbyUser.js b/jam-ui/src/components/sessions/JKLobbyUser.js index 691c38788..30f5784e2 100644 --- a/jam-ui/src/components/sessions/JKLobbyUser.js +++ b/jam-ui/src/components/sessions/JKLobbyUser.js @@ -1,24 +1,29 @@ import React, { useState } from 'react'; -import { Row } from 'reactstrap'; import JKProfileAvatar from '../profile/JKProfileAvatar'; import JKConnectButton from '../profile/JKConnectButton'; import JKMessageButton from '../profile/JKMessageButton'; import JKMoreDetailsButton from '../profile/JKMoreDetailsButton'; +import JKLatencyBadge from '../profile/JKLatencyBadge'; +import JKProfileInstrumentsList from '../profile/JKProfileInstrumentsList'; +import { useTranslation } from 'react-i18next'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import JKProfileSidePanel from '../profile/JKProfileSidePanel'; import { useSelector, useDispatch } from 'react-redux'; import { fetchPerson } from '../../store/features/peopleSlice'; +import { useResponsive } from '@farfetch/react-context-responsive'; import { useAuth } from '../../context/UserAuth'; -function JKLobbyUser({ user }) { - const [showSidePanel, setShowSidePanel] = useState(false) +function JKLobbyUser({ user, setSelectedUsers }) { + const { t } = useTranslation(); + const [showSidePanel, setShowSidePanel] = useState(false); const { currentUser } = useAuth(); const dispatch = useDispatch(); + const { greaterThan } = useResponsive(); const latencyData = useSelector(state => state.latency.latencies.find(l => l.user_id === user.id)); const userData = useSelector(state => state.people.people.find(p => p.id === user.id)); - const toggleMoreDetails = async (e) => { + const toggleMoreDetails = async e => { e.preventDefault(); try { await dispatch(fetchPerson({ userId: user.id })).unwrap(); @@ -28,43 +33,105 @@ function JKLobbyUser({ user }) { setShowSidePanel(prev => !prev); }; + const setSelection = e => { + if (e.target.checked) { + setSelectedUsers(prev => [...prev, user.id]); + } else { + setSelectedUsers(prev => prev.filter(u => u !== user.id)); + } + }; + return ( <> - - - - -
- - - -
- -
- - - } - removeContent={} - cssClasses="fs--1 px-2 py-1 mr-1" - /> + {greaterThan.sm ? ( + + +
+
+ +
+
+
+
+ +
+ +
+
+ {t('person_attributes.latency_to_me', { ns: 'people' })}:{' '} + +
+
+ {t('person_attributes.instruments', { ns: 'people' })} + {/* */} +
+
+
+ - - - + + } + removeContent={} + cssClasses="fs--1 px-2 py-1 mr-1" + /> - - - - - - + + + + + + + + + + ) : ( + <> +
+ {t('person_attributes.latency_to_me', { ns: 'people' })}:{' '} + +
+
+
{t('person_attributes.instruments', { ns: 'people' })}
+ {/* */} +
+
+
{t('person_attributes.genres', { ns: 'people' })}
+ {/* */} +
+
+ + } + removeContent={} + cssClasses="fs--1 px-2 py-1 mr-1" + /> + + + + + + + + + + + + )} + + ); } diff --git a/jam-ui/src/components/sessions/JKLobbyUserList.js b/jam-ui/src/components/sessions/JKLobbyUserList.js index c34028b1d..267830d5f 100644 --- a/jam-ui/src/components/sessions/JKLobbyUserList.js +++ b/jam-ui/src/components/sessions/JKLobbyUserList.js @@ -5,9 +5,9 @@ import JKLobbyUser from './JKLobbyUser'; import { isIterableArray } from '../../helpers/utils'; import Loader from '../common/Loader'; -function JKLobbyUserList({ loadingStatus, onlineMusicians}) { +function JKLobbyUserList({ loadingStatus, onlineMusicians, selectedUsers, setSelectedUsers }) { const { t } = useTranslation(); - + return ( <> {loadingStatus === 'loading' && onlineMusicians.length === 0 ? ( @@ -16,7 +16,7 @@ function JKLobbyUserList({ loadingStatus, onlineMusicians}) { - {isIterableArray(onlineMusicians) ? ( - onlineMusicians.map(musician => ) + onlineMusicians.map(musician => ) ) : ( diff --git a/jam-ui/src/components/sessions/JKLobbyUserSwiper.js b/jam-ui/src/components/sessions/JKLobbyUserSwiper.js new file mode 100644 index 000000000..62d22b557 --- /dev/null +++ b/jam-ui/src/components/sessions/JKLobbyUserSwiper.js @@ -0,0 +1,75 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import JKProfileAvatar from '../profile/JKProfileAvatar'; + +// 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 JKLobbyUser from '../sessions/JKLobbyUser'; + +// 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'; + +SwiperCore.use([Navigation, Pagination, Scrollbar, A11y]); + +const JKLobbyUserSwiper = ({ onlineMusicians, setSelectedUsers, loadingStatus }) => { + return ( + <> + console.log('slide change')} + onSlideNextTransitionEnd={swiper => { + if(swiper.isEnd){ + //goNextPage() + } + }} + pagination={{ + clickable: true, + type: 'custom' + }} + navigation={{ + nextEl: '.swiper-button-next', + prevEl: '.swiper-button-prev' + }} + > + {onlineMusicians.map((musician, index) => ( + + + + +
+ +
+
{musician.name}
+
+ + + +
+
+ ))} +
+
+
+
+
+
+ + ); +}; + +JKLobbyUserSwiper.propTypes = { + onlineMusicians: PropTypes.arrayOf(PropTypes.instanceOf(Object)).isRequired, + setSelectedUsers: PropTypes.func.isRequired, +}; + +export default JKLobbyUserSwiper; diff --git a/jam-ui/src/config.js b/jam-ui/src/config.js index da6540969..41657ece1 100644 --- a/jam-ui/src/config.js +++ b/jam-ui/src/config.js @@ -15,4 +15,9 @@ export const settings = { isNavbarVerticalCollapsed: false, navbarStyle: 'transparent' }; -export default { version, navbarBreakPoint, topNavbarBreakpoint, settings }; +export const sessionPrivacyMap = { + public: 1, + private_invite: 2, + private_approve: 3 +}; +export default { version, navbarBreakPoint, topNavbarBreakpoint, settings, sessionPrivacyMap }; diff --git a/jam-ui/src/i18n/locales/en/sessions.json b/jam-ui/src/i18n/locales/en/sessions.json index 9e632af0c..6f81d3e27 100644 --- a/jam-ui/src/i18n/locales/en/sessions.json +++ b/jam-ui/src/i18n/locales/en/sessions.json @@ -44,6 +44,13 @@ "header": { "musician": "Musician", "actions": "Actions" + }, + "chat_notifications": { + "title": "Lobby Chat Notifications", + "body": "To avoid missing lobby chat messages when you’re not actively watching this page, you can give us permission to give you browser notifications when this page is open in a tab on your browser, but you’re not currently looking at this tab/page", + "dont_ask_again": "Don’t ask me again", + "grant_permission": "Grant Permission", + "no_thanks": "Not Now. Thanks" } } } \ No newline at end of file diff --git a/jam-ui/src/store/features/lobbyChatMessagesSlice.js b/jam-ui/src/store/features/lobbyChatMessagesSlice.js index 62b3b8ddb..e41a23547 100644 --- a/jam-ui/src/store/features/lobbyChatMessagesSlice.js +++ b/jam-ui/src/store/features/lobbyChatMessagesSlice.js @@ -34,19 +34,13 @@ const chatMessagesSlice = createSlice({ state.status = 'loading'; }) .addCase(fetchLobbyChatMessages.fulfilled, (state, action) => { - console.log('_DEBUG_1 fetchLobbyChatMessages', action.payload); - //let chats = [...state.records.messages, ...action.payload.chats]; const lastOnly = action.meta.arg.lastOnly; - console.log('_DEBUG_2 fetchLobbyChatMessages', lastOnly); state.records = { next: state.records.next === null && lastOnly? null : action.payload.next, messages: action.payload.chats.map(m => ({...m, status: 'delivered'})).sort((a, b) => { return new Date(a.created_at) - new Date(b.created_at); }) }; - // state.offset_messages = action.payload.chats.sort((a, b) => { - // return new Date(a.created_at) - new Date(b.created_at); - // }); state.status = 'succeeded'; }) .addCase(fetchLobbyChatMessages.rejected, (state, action) => {
+ {t('lobby.header.musician', { ns: 'sessions' })} @@ -26,7 +26,7 @@ function JKLobbyUserList({ loadingStatus, onlineMusicians}) {
No users currently online.