now notifications in header drawer scrollable

This commit is contained in:
Nuwan 2024-09-06 23:15:48 +05:30
parent 499cd7e16b
commit ef79d3a8c0
14 changed files with 190 additions and 114 deletions

View File

@ -9,24 +9,23 @@ import { isIterableArray } from '../../helpers/utils';
import FalconCardHeader from '../common/FalconCardHeader'; import FalconCardHeader from '../common/FalconCardHeader';
import Notification from '../notification/JKNotification'; import Notification from '../notification/JKNotification';
import { Scrollbar } from 'react-scrollbars-custom'; import { Scrollbar } from 'react-scrollbars-custom';
import { fetchNotifications } from '../../store/features/notificationSlice';
import { useDispatch, useSelector } from 'react-redux';
import { useAuth } from '../../context/UserAuth'; import { useAuth } from '../../context/UserAuth';
import { readNotifications } from '../../helpers/rest'; import { readNotifications } from '../../helpers/rest';
import useNotifications from '../../hooks/useNotifications';
const JKNotificationDropdown = () => { const JKNotificationDropdown = () => {
const { currentUser, isAuthenticated } = useAuth(); const { currentUser, isAuthenticated } = useAuth();
const dispatch = useDispatch(); const {
const notifications = useSelector(state => state.notification.notifications); notifications,
const next = useSelector(state => state.notification.next); offset,
const status = useSelector(state => state.notification.status); setOffset,
const unread_total = useSelector(state => state.notification.unread_total); next,
unread_total,
loadNotifications,
} = useNotifications(currentUser);
const LIMIT = 20; const LIMIT = 20;
const MAX_COUNT_ON_BADGE = 99; const MAX_COUNT_ON_BADGE = 99;
const [offset, setOffset] = useState(0);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isAllRead, setIsAllRead] = useState(false); const [isAllRead, setIsAllRead] = useState(false);
@ -39,19 +38,6 @@ const JKNotificationDropdown = () => {
setIsOpen(!isOpen); setIsOpen(!isOpen);
}; };
const loadNotifications = async () => {
try {
const options = {
userId: currentUser.id,
offset: offset,
limit: LIMIT
};
await dispatch(fetchNotifications(options)).unwrap();
} catch (error) {
console.log(error);
}
};
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
readNotifications(currentUser.id) readNotifications(currentUser.id)
@ -65,11 +51,12 @@ const JKNotificationDropdown = () => {
}, [isOpen]); }, [isOpen]);
useEffect(() => { useEffect(() => {
loadNotifications(); if(isAuthenticated)
}, []); loadNotifications();
}, [currentUser]);
useEffect(() => { useEffect(() => {
if (offset > 0 && next !== null) { if (isAuthenticated && offset > 0 && next !== null) {
loadNotifications(); loadNotifications();
} }
}, [offset]); }, [offset]);
@ -107,9 +94,16 @@ const JKNotificationDropdown = () => {
})} })}
> >
{isIterableArray(notifications) && notifications.length > 0 && !isAllRead && unread_total > 0 && ( {isIterableArray(notifications) && notifications.length > 0 && !isAllRead && unread_total > 0 && (
<div className="num-circle" onClick={handleToggle}>{unread_total < MAX_COUNT_ON_BADGE ? unread_total : `${MAX_COUNT_ON_BADGE}+`}</div> <div className="num-circle" onClick={handleToggle}>
{unread_total < MAX_COUNT_ON_BADGE ? unread_total : `${MAX_COUNT_ON_BADGE}+`}
</div>
)} )}
<FontAwesomeIcon icon={['fas', 'bell']} transform="shrink-5" className="fs-4 bell-icon" onClick={handleToggle} /> <FontAwesomeIcon
icon={['fas', 'bell']}
transform="shrink-5"
className="fs-4 bell-icon"
onClick={handleToggle}
/>
</DropdownToggle> </DropdownToggle>
<DropdownMenu right className="dropdown-menu-card" data-testid="notificationDropdown"> <DropdownMenu right className="dropdown-menu-card" data-testid="notificationDropdown">
<Card className="card-notification shadow-none" style={{ maxWidth: '20rem' }}> <Card className="card-notification shadow-none" style={{ maxWidth: '20rem' }}>
@ -129,7 +123,7 @@ const JKNotificationDropdown = () => {
mobileNative={true} mobileNative={true}
trackClickBehavior="step" trackClickBehavior="step"
> >
{isIterableArray(notifications) && {isIterableArray(notifications) &&
notifications.map(notification => ( notifications.map(notification => (
<ListGroupItem <ListGroupItem
key={`notification-drop-item-${notification.notification_id}`} key={`notification-drop-item-${notification.notification_id}`}

View File

@ -1,4 +1,4 @@
import React, { useEffect} from 'react'; import React from 'react';
import ProfileAvatar from '../profile/JKProfileAvatar' import ProfileAvatar from '../profile/JKProfileAvatar'
import TimeAgo from '../common/JKTimeAgo'; import TimeAgo from '../common/JKTimeAgo';
import { useAuth } from '../../context/UserAuth'; import { useAuth } from '../../context/UserAuth';
@ -34,11 +34,6 @@ function JKFriendRequestNotification(props) {
}; };
useEffect(() => {
if (!user)
dispatch(fetchPerson({ userId: source_user_id }))
}, []);
return ( return (
<> <>
<div className="notification-avatar mr-3"> <div className="notification-avatar mr-3">

View File

@ -1,24 +1,16 @@
import React, { useEffect } from 'react' import React from 'react'
import ProfileAvatar from '../profile/JKProfileAvatar' import ProfileAvatar from '../profile/JKProfileAvatar'
import TimeAgo from '../common/JKTimeAgo'; import TimeAgo from '../common/JKTimeAgo';
import useUserProfile from '../../hooks/useUserProfile'; import useUserProfile from '../../hooks/useUserProfile';
import { useDispatch, useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { fetchPerson } from '../../store/features/peopleSlice';
const JKGenericNotification = (notification) => { const JKGenericNotification = (notification) => {
const {formatted_msg, created_at, source_user_id} = notification; const {formatted_msg, created_at, source_user_id} = notification;
const dispatch = useDispatch();
const user = useSelector(state => state.people.people.find(person => person.id === source_user_id)); const user = useSelector(state => state.people.people.find(person => person.id === source_user_id));
const { photoUrl } = useUserProfile(user); // user is the person who sent the message const { photoUrl } = useUserProfile(user); // user is the person who sent the message
useEffect(() => {
dispatch(fetchPerson({ userId: source_user_id }))
}, []);
return ( return (
<> <>
<div className="notification-avatar mr-3"> <div className="notification-avatar mr-3">

View File

@ -5,7 +5,6 @@ import {useAuth} from '../../context/UserAuth';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { removeNotification } from '../../store/features/notificationSlice'; import { removeNotification } from '../../store/features/notificationSlice';
import JKGenericNotification from './JKGenericNotification'; import JKGenericNotification from './JKGenericNotification';
import JKFriendRequestNotification from './JKFriendRequestNotification'; import JKFriendRequestNotification from './JKFriendRequestNotification';
import TextMessageNotification from './JKTextMessageNotification'; import TextMessageNotification from './JKTextMessageNotification';

View File

@ -1,7 +1,6 @@
import React, { useEffect } from 'react'; import React from 'react';
import { useAuth } from '../../context/UserAuth'; import { useAuth } from '../../context/UserAuth';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { fetchPerson, add as addPerson } from '../../store/features/peopleSlice';
import JKMessageButton from '../profile/JKMessageButton'; import JKMessageButton from '../profile/JKMessageButton';
import ProfileAvatar from '../profile/JKProfileAvatar'; import ProfileAvatar from '../profile/JKProfileAvatar';
import TimeAgo from '../common/JKTimeAgo'; import TimeAgo from '../common/JKTimeAgo';
@ -10,19 +9,10 @@ import useUserProfile from '../../hooks/useUserProfile';
function JKTextMessageNotification(props) { function JKTextMessageNotification(props) {
const { source_user, source_user_id, message, created_at } = props.notification; const { source_user, source_user_id, message, created_at } = props.notification;
const { currentUser } = useAuth(); const { currentUser } = useAuth();
const dispatch = useDispatch();
const user = useSelector(state => state.people.people.find(person => person.id === source_user_id)); const user = useSelector(state => state.people.people.find(person => person.id === source_user_id));
const { photoUrl } = useUserProfile(user); // user is the person who sent the message const { photoUrl } = useUserProfile(user); // user is the person who sent the message
useEffect(() => {
if(!user)
dispatch(fetchPerson({ userId: source_user_id }))
}, []);
return ( return (
<> <>
<div className="notification-avatar mr-3"> <div className="notification-avatar mr-3">

View File

@ -5,35 +5,45 @@ import FalconCardHeader from '../common/FalconCardHeader';
import Loader from '../common/Loader'; import Loader from '../common/Loader';
import { isIterableArray } from '../../helpers/utils'; import { isIterableArray } from '../../helpers/utils';
import { fetchNotifications } from '../../store/features/notificationSlice';
import { useDispatch, useSelector } from 'react-redux';
import { useAuth } from '../../context/UserAuth'; import { useAuth } from '../../context/UserAuth';
import useNotifications from '../../hooks/useNotifications';
const JKNotifications = () => { const JKNotifications = () => {
const { currentUser } = useAuth(); const { currentUser, isAuthenticated } = useAuth();
const dispatch = useDispatch(); const {
const notifications = useSelector(state => state.notification.notifications); notifications,
const loadingState = useSelector(state => state.notification.state); offset,
setOffset,
const LIMIT = 20; next,
const [page, setPage] = useState(0); unread_total,
loadNotifications,
const loadNotifications = async () => { notificationStatus: loadingState
try { } = useNotifications(currentUser);
const options = {
userId: currentUser.id,
offset: page * LIMIT,
limit: LIMIT
};
await dispatch(fetchNotifications(options)).unwrap();
//setPage(prev => prev + 1);
} catch (error) {
console.log(error);
}
};
useEffect(() => { useEffect(() => {
loadNotifications(); if(isAuthenticated)
loadNotifications();
}, [currentUser]);
useEffect(() => {
if (isAuthenticated && offset > 0 && next !== null) {
loadNotifications();
}
}, [offset]);
useEffect(() => {
const onscroll = () => {
console.log("scrolling", window.scrollY, window.innerHeight, document.body.scrollHeight);
const scrolledTo = window.scrollY + window.innerHeight;
const isReachBottom = document.body.scrollHeight === scrolledTo;
if (isReachBottom) {
setOffset(offset + 1);
}
};
window.addEventListener("scroll", onscroll);
return () => {
window.removeEventListener("scroll", onscroll);
};
}, []); }, []);
return ( return (

View File

@ -35,7 +35,7 @@ const JKMessageButton = props => {
return ( return (
<> <>
<JKMessageModal show={showModal} setShow={setShowModal} user={user} currentUser={currentUser} /> <JKMessageModal show={showModal} setShow={setShowModal} user={user} />
<Button <Button
id={"text-message-user-" + user.id} id={"text-message-user-" + user.id}
onClick={() => setShowModal(!showModal)} onClick={() => setShowModal(!showModal)}

View File

@ -8,6 +8,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { fetchMessagesByReceiverId, postNewMessage } from '../../store/features/textMessagesSlice'; import { fetchMessagesByReceiverId, postNewMessage } from '../../store/features/textMessagesSlice';
import { isIterableArray } from '../../helpers/utils'; import { isIterableArray } from '../../helpers/utils';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import useUserProfile from '../../hooks/useUserProfile';
const JKMessageModal = props => { const JKMessageModal = props => {
const { show, setShow, user } = props; const { show, setShow, user } = props;
@ -26,6 +27,9 @@ const JKMessageModal = props => {
const messageTextBox = useRef(); const messageTextBox = useRef();
const scrolledToBottom = useRef(false); const scrolledToBottom = useRef(false);
const { photoUrl: userPhotoUrl } = useUserProfile(user);
const { photoUrl: currentUserPhotoUrl } = useUserProfile(currentUser);
const messages = useSelector(state => const messages = useSelector(state =>
state.textMessage.messages state.textMessage.messages
.filter( .filter(
@ -150,7 +154,7 @@ const JKMessageModal = props => {
<div className="d-flex mb-3 mr-1 text-message-row" key={message.id}> <div className="d-flex mb-3 mr-1 text-message-row" key={message.id}>
<div className="avatar avatar-2xl d-inline-block"> <div className="avatar avatar-2xl d-inline-block">
<JKProfileAvatar <JKProfileAvatar
url={message.receiverId === currentUser.id ? currentUser.photo_url : user.photo_url} src={message.receiverId === currentUser.id ? userPhotoUrl : currentUserPhotoUrl }
/> />
</div> </div>
<div className="d-inline-block"> <div className="d-inline-block">

View File

@ -13,6 +13,7 @@ import useOnScreen from '../../hooks/useOnScreen';
import useKeepScrollPosition from '../../hooks/useKeepScrollPosition'; import useKeepScrollPosition from '../../hooks/useKeepScrollPosition';
import { useLobbyChat } from './JKLobbyChatContext'; import { useLobbyChat } from './JKLobbyChatContext';
import useUserProfile from '../../hooks/useUserProfile';
function JKLobbyChat() { function JKLobbyChat() {
const CHANNEL_LOBBY = 'lobby'; const CHANNEL_LOBBY = 'lobby';
@ -28,6 +29,8 @@ function JKLobbyChat() {
const [messagesArrived, setMessagesArrived] = useState(false); const [messagesArrived, setMessagesArrived] = useState(false);
//const [offset, setOffset] = useState(0); //const [offset, setOffset] = useState(0);
const userProfile = useUserProfile(currentUser);
const { t } = useTranslation('sessions'); const { t } = useTranslation('sessions');
const chatMessages = useSelector(state => state.lobbyChat.records.messages); const chatMessages = useSelector(state => state.lobbyChat.records.messages);
@ -165,7 +168,7 @@ function JKLobbyChat() {
<div className="d-flex mb-3 mr-1 text-message-row" key={greaterThan ? `desktop_${message.id}` : `mobile_${message.id}`}> <div className="d-flex mb-3 mr-1 text-message-row" key={greaterThan ? `desktop_${message.id}` : `mobile_${message.id}`}>
<div className='d-flex align-items-center' ref={ref => (i === 0 ? setLastMessageRef(ref) : null)}> <div className='d-flex align-items-center' ref={ref => (i === 0 ? setLastMessageRef(ref) : null)}>
<div className="avatar avatar-2xl"> <div className="avatar avatar-2xl">
<JKProfileAvatar url={message.user.photo_url} /> <JKProfileAvatar src={message.user.photo_url} />
</div> </div>
<div className="pt-2"> <div className="pt-2">
<div className="d-flex flex-column"> <div className="d-flex flex-column">

View File

@ -0,0 +1,67 @@
import { useState, useEffect } from 'react';
import { fetchNotifications } from '../store/features/notificationSlice';
import { fetchPeopleByIds } from '../store/features/peopleSlice';
import { useDispatch, useSelector } from 'react-redux';
const useNotifications = user => {
const LIMIT = 20;
const [offset, setOffset] = useState(0);
const [notifications, setNotifications] = useState([]);
const reduxNotifications = useSelector(state => state.notification.notifications);
const next = useSelector(state => state.notification.next);
const notificationStatus = useSelector(state => state.notification.status);
const unread_total = useSelector(state => state.notification.unread_total);
const peopleStatus = useSelector(state => state.people.status);
const people = useSelector(state => state.people.people);
const dispatch = useDispatch();
const loadNotifications = async () => {
const options = {
userId: user.id,
offset: offset,
limit: LIMIT
};
try {
await dispatch(fetchNotifications(options)).unwrap();
} catch (error) {
console.log(error);
}
};
const fetchNotificationSourceUsers = async () => {
const sourceUserIds = reduxNotifications.map(notification => notification.source_user_id).filter((value, index, self) => self.indexOf(value) === index);
const options = { userId: user.id, ids: sourceUserIds };
try {
await dispatch(fetchPeopleByIds(options)).unwrap();
} catch (error) {
console.log(error);
}
};
useEffect(() => {
if (reduxNotifications && reduxNotifications.length && notificationStatus === 'succeeded') {
fetchNotificationSourceUsers();
}
}, [reduxNotifications, notificationStatus]);
useEffect(() => {
const newNotifications = reduxNotifications.filter(notification => !notifications.find(n => n.id === notification.id));
setNotifications(prev => [...prev, ...newNotifications]);
}, [people]);
return {
notifications,
offset,
setOffset,
loadNotifications,
next,
unread_total,
notificationStatus,
peopleStatus
};
};
export default useNotifications;

View File

@ -1,44 +1,46 @@
import { getPersonById } from '../helpers/rest';
import { useEffect, useState, useMemo } from 'react'; import { useEffect, useState, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchPerson } from '../store/features/peopleSlice';
const useUserProfile = (user) => { const useUserProfile = user => {
const [userProfile, setUserProfile] = useState(null) const [userProfile, setUserProfile] = useState(null);
const people = useSelector(state => state.people.people);
const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
if (!user) { if (!user) {
setUserProfile(null); setUserProfile(null);
return; return;
} }
getPersonById(user.id) const person = people.find(person => person.id === user.id);
.then(response => { if (person) {
if (response.ok) { setUserProfile(person);
return response.json(); } else {
} dispatch(fetchPerson({ userId: user.id }))
}) .unwrap()
.then(data => { .then(resp => {
setUserProfile(data) setUserProfile(resp);
}) });
.catch(error => console.error(error)); }
return () => { return () => {
setUserProfile(null); setUserProfile(null);
} };
}, [user]); }, [user]);
const photoUrl = useMemo(() => { const photoUrl = useMemo(() => {
if(userProfile && userProfile.v2_photo_uploaded){ if (userProfile && userProfile.v2_photo_uploaded) {
return userProfile.v2_photo_url return userProfile.v2_photo_url;
}else if(userProfile && !userProfile.v2_photo_uploaded){ } else if (userProfile && !userProfile.v2_photo_uploaded) {
return user.photo_url return user.photo_url;
} }
return null return null;
}, [userProfile]) }, [userProfile]);
return{ return {
userProfile, userProfile,
photoUrl photoUrl
} };
} };
export default useUserProfile; export default useUserProfile;

View File

@ -1,6 +1,5 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import {getNotifications, deleteNotification} from '../../helpers/rest' import {getNotifications, deleteNotification} from '../../helpers/rest'
const initialState = { const initialState = {
notifications: [], notifications: [],
next: null, next: null,

View File

@ -1,5 +1,5 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit" import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import { getPeople, getPersonById, acceptFriendRequest as accept } from '../../helpers/rest'; import { getPeople, getPeopleByIds, getPersonById, acceptFriendRequest as accept } from '../../helpers/rest';
const initialState = { const initialState = {
people: [], people: [],
@ -21,12 +21,19 @@ export const fetchPeople = createAsyncThunk(
export const preFetchPeople = createAsyncThunk( export const preFetchPeople = createAsyncThunk(
'people/preFetchPeople', 'people/preFetchPeople',
async (options, thunkAPI) => { async (options, thunkAPI) => {
const response = await getPeople(options) const response = await getPeople(options)
return response.json() return response.json()
} }
) )
export const fetchPeopleByIds = createAsyncThunk(
'people/fetchPeopleByIds',
async (options, thunkAPI) => {
const response = await getPeopleByIds(options)
return response.json()
}
)
export const fetchPerson = createAsyncThunk( export const fetchPerson = createAsyncThunk(
'people/fetchPerson', 'people/fetchPerson',
async (options, thunkAPI) => { async (options, thunkAPI) => {
@ -135,6 +142,20 @@ export const peopleSlice = createSlice({
state.people.push(action.payload) 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'
})
} }
}) })

View File

@ -25,7 +25,7 @@ node :is_blank_filter do |foo|
end end
child(:results => :musicians) { child(:results => :musicians) {
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :online, :musician, :photo_url, :biography, :regionname, :score, :full_score, :is_friend, :is_following, :pending_friend_request, :last_active_timestamp attributes :id, :first_name, :last_name, :name, :city, :state, :country, :online, :musician, :photo_url, :biography, :regionname, :score, :full_score, :is_friend, :is_following, :pending_friend_request, :last_active_timestamp, :v2_photo_url, :v2_photo_uploaded
# node :is_friend do |musician| # node :is_friend do |musician|
# @search.is_friend?(musician) # @search.is_friend?(musician)