From b8abb6674a7b189e366cdc68db9ba0f266a198e4 Mon Sep 17 00:00:00 2001 From: Abdelruhman Sami <121282837+AbdelruhmanSamy@users.noreply.github.com> Date: Sun, 15 Dec 2024 23:26:34 +0200 Subject: [PATCH] :sparkles: (sidebar): introducing right sidebar from same component (#126) --- app/src/components/AppLayout.tsx | 24 +++- app/src/components/BackArrow.tsx | 10 +- app/src/components/UsersList.tsx | 5 +- app/src/components/side-bar/SideBar.tsx | 111 ++++++++++-------- .../components/side-bar/SideBarContext.tsx | 13 ++ .../side-bar/chats/SideBarToolsButton.tsx | 12 +- .../side-bar/settings/OptionsList.tsx | 10 +- .../settings/SettingsSideBarHeader.tsx | 14 ++- .../settings/side-bar-row/SideBarRow.tsx | 13 +- .../settings/side-bar-row/getDataFactory.ts | 12 +- app/src/features/chats/StartNewChat.tsx | 6 +- app/src/features/chats/Topbar.tsx | 16 ++- app/src/features/groups/CreateGroupForm.tsx | 10 +- .../features/privacy-settings/BlockList.tsx | 8 +- .../profile-settings/ProfileSettings.tsx | 10 +- app/src/state/side-bar/sideBar.ts | 42 +++---- 16 files changed, 207 insertions(+), 109 deletions(-) create mode 100644 app/src/components/side-bar/SideBarContext.tsx diff --git a/app/src/components/AppLayout.tsx b/app/src/components/AppLayout.tsx index 1f826e0b..98e543c9 100644 --- a/app/src/components/AppLayout.tsx +++ b/app/src/components/AppLayout.tsx @@ -3,8 +3,12 @@ import styled from "styled-components"; import Main from "./Main"; import SideBar from "./side-bar/SideBar"; import { DESKTOP_VIEW, MOBILE_VIEW } from "@constants"; +import { useState } from "react"; -const StyledApp = styled.div<{ $isChatOpen: boolean }>` +const StyledApp = styled.div<{ + $isChatOpen: boolean; + $isRightSideBarOpen: boolean; +}>` @media ${MOBILE_VIEW} { & > main { display: ${({ $isChatOpen }) => ($isChatOpen ? "contents" : "none")}; @@ -17,7 +21,10 @@ const StyledApp = styled.div<{ $isChatOpen: boolean }>` @media ${DESKTOP_VIEW} { display: grid; - grid-template-columns: minmax(20rem, 2.5fr) 5fr; + grid-template-columns: ${({ $isRightSideBarOpen }) => + $isRightSideBarOpen ? "1.5fr 3fr 1.5fr" : "1.5fr 4.5fr"}; + + overflow-x: hidden; & > main { display: block; @@ -34,12 +41,19 @@ function AppLayout() { const isChatOpen = !!chatId; + const [showRightSideBar, setShowRightSideBar] = useState(false); + return ( - - + +
- +
+ {showRightSideBar && }
); } diff --git a/app/src/components/BackArrow.tsx b/app/src/components/BackArrow.tsx index c62ba7a2..fecbda3b 100644 --- a/app/src/components/BackArrow.tsx +++ b/app/src/components/BackArrow.tsx @@ -2,6 +2,7 @@ import styled from "styled-components"; import { getIcon } from "@data/icons"; import { useAppDispatch, useAppSelector } from "@hooks/useGlobalState"; import { updateSideBarView } from "@state/side-bar/sideBar"; +import { useSidebarType } from "./side-bar/SideBarContext"; const StyledArrow = styled.div` width: 30px; @@ -20,14 +21,19 @@ const StyledArrow = styled.div` function BackArrow() { const dispatch = useAppDispatch(); - const { backView } = useAppSelector((state) => state.sideBarData); + const type = useSidebarType(); + const { backView } = useAppSelector((state) => + type === "left" + ? state.sideBarData.leftSideBar + : state.sideBarData.rightSideBar + ); return ( backView !== undefined && - dispatch(updateSideBarView({ redirect: backView })) + dispatch(updateSideBarView({ redirect: backView, data: { type } })) } > {getIcon("BackArrow")} diff --git a/app/src/components/UsersList.tsx b/app/src/components/UsersList.tsx index e298b124..d6b47df0 100644 --- a/app/src/components/UsersList.tsx +++ b/app/src/components/UsersList.tsx @@ -10,6 +10,7 @@ import { updateSideBarView } from "@state/side-bar/sideBar"; import { useAllUsers } from "@features/groups/hooks/useAllUsers"; import { useUser } from "@features/authentication/login/hooks/useUser"; +import { useSidebarType } from "./side-bar/SideBarContext"; const Container = styled.div` width: 100%; @@ -39,13 +40,13 @@ function UsersList({ type }: { type: "channel" | "group" }) { const { user: currentUser, isPending: isPendingCurrentUser } = useUser(); const [searchQuery, setSearchQuery] = useState(""); const dispatch = useAppDispatch(); - + const sideBarType = useSidebarType(); if (isPendenigAllUsers || isPendingCurrentUser) return null; function handleClick() { const redirect = type === "channel" ? sideBarPages.NEW_CHANNEL : sideBarPages.NEW_GROUP; - dispatch(updateSideBarView({ redirect })); + dispatch(updateSideBarView({ redirect, data: { type: sideBarType } })); } const filteredUsers = users?.filter( diff --git a/app/src/components/side-bar/SideBar.tsx b/app/src/components/side-bar/SideBar.tsx index 86968975..df837d92 100644 --- a/app/src/components/side-bar/SideBar.tsx +++ b/app/src/components/side-bar/SideBar.tsx @@ -19,6 +19,7 @@ import { useAppSelector } from "@hooks/useGlobalState"; import BlockList from "@features/privacy-settings/BlockList"; import Devices from "@features/devices/Devices"; import NewGroup from "@features/groups/NewGroup.js"; +import { SidebarContext } from "./SideBarContext.js"; interface SideBarProps { rows?: SideBarRowProps[]; @@ -65,57 +66,63 @@ const StyledSidebar = styled.aside<{ $isExiting: boolean }>` } `; -const sideBarMap: { [key: string]: (props: SideBarProps) => React.ReactNode } = - { - Chats: () => ( - - - - ), - Contacts: () => , - Settings: (props) => ( - - - - - ), - Privacy: (props) => , - SettingsUpdate: (props) => ( - - {props.data && } - - ), - ProfileUpdate: () => , - blockList: () => ( - - - - ), - Devices: () => } />, - AddMembers: (props) => ( - - - - ), - NewGroup: () => ( - - - - ), - NewChannel: () => ( - - - - ), - }; - -function Sidebar() { - const { page, props } = useAppSelector((state) => state.sideBarData); +const sideBarMap: { + [key: string]: ( + props: SideBarProps, + type: "left" | "right" + ) => React.ReactNode; +} = { + Chats: () => ( + + + + ), + Contacts: () => , + Settings: (props) => ( + + + + + ), + Privacy: (props) => , + SettingsUpdate: (props) => ( + + {props.data && } + + ), + ProfileUpdate: () => , + blockList: () => ( + + + + ), + Devices: () => } />, + AddMembers: (props) => ( + + + + ), + NewGroup: () => ( + + + + ), + NewChannel: () => ( + + + + ), +}; + +function Sidebar({ type }: { type: "left" | "right" }) { + const { page, props } = useAppSelector((state) => + type === "left" + ? state.sideBarData.leftSideBar + : state.sideBarData.rightSideBar + ); const [currentPage, setCurrentPage] = useState(page); const [isExiting, setIsExiting] = useState(false); - console.log(currentPage); - useEffect(() => { if (currentPage !== page) { setIsExiting(true); @@ -133,9 +140,11 @@ function Sidebar() { const pageString = pagesMap[currentPage] || "Settings"; return ( - - {sideBarMap[pageString](props || {})} - + + + {sideBarMap[pageString](props!, type)} + + ); } diff --git a/app/src/components/side-bar/SideBarContext.tsx b/app/src/components/side-bar/SideBarContext.tsx new file mode 100644 index 00000000..d25f857b --- /dev/null +++ b/app/src/components/side-bar/SideBarContext.tsx @@ -0,0 +1,13 @@ +import { createContext, useContext } from "react"; + +type SidebarType = "left" | "right"; + +export const SidebarContext = createContext(undefined); + +export const useSidebarType = () => { + const context = useContext(SidebarContext); + if (!context) { + throw new Error("useSidebarType must be used within a SidebarProvider"); + } + return context; +}; diff --git a/app/src/components/side-bar/chats/SideBarToolsButton.tsx b/app/src/components/side-bar/chats/SideBarToolsButton.tsx index 5f7b6dd8..11c95e25 100644 --- a/app/src/components/side-bar/chats/SideBarToolsButton.tsx +++ b/app/src/components/side-bar/chats/SideBarToolsButton.tsx @@ -8,6 +8,7 @@ import { updateSideBarView } from "@state/side-bar/sideBar"; import { sideBarPages } from "@data/sideBar"; import { useMouseLeave } from "@hooks/useMouseLeave"; import { getIcon } from "@data/icons"; +import { useSidebarType } from "../SideBarContext"; import { RootState } from "@state/store"; import { clearSearch } from "@state/messages/global-search"; import { useSelector } from "react-redux"; @@ -58,6 +59,9 @@ function SettingsToolbar() { dispatch(clearSearch()); }; + + const type = useSidebarType(); + return ( <> @@ -96,8 +100,8 @@ function SettingsToolbar() { dispatch( updateSideBarView({ redirect: sideBarPages.SETTINGS, - data: undefined, - }), + data: { type }, + }) ) } /> diff --git a/app/src/components/side-bar/settings/OptionsList.tsx b/app/src/components/side-bar/settings/OptionsList.tsx index 93a4c034..d6ea940f 100644 --- a/app/src/components/side-bar/settings/OptionsList.tsx +++ b/app/src/components/side-bar/settings/OptionsList.tsx @@ -9,6 +9,7 @@ import { StatusType, } from "types/sideBar"; import { statusMap } from "@data/sideBar"; +import { useSidebarType } from "../SideBarContext"; const StyledOptionsList = styled.div` display: flex; @@ -32,7 +33,7 @@ function getKey( | permissionSettingsID | undefined, type: StatusType | undefined, - index: number, + index: number ) { let key; if (status !== undefined && type !== undefined) { @@ -48,8 +49,13 @@ function getKey( } function OptionsList({ rows }: { rows: SideBarRowProps[] }) { - const { title } = useAppSelector((state) => state.sideBarData); + const type = useSidebarType(); + const { title } = useAppSelector((state) => + type === "left" + ? state.sideBarData.leftSideBar + : state.sideBarData.rightSideBar + ); return ( <> {rows.length > 0 && ( diff --git a/app/src/components/side-bar/settings/SettingsSideBarHeader.tsx b/app/src/components/side-bar/settings/SettingsSideBarHeader.tsx index 73ae870e..d84406f0 100644 --- a/app/src/components/side-bar/settings/SettingsSideBarHeader.tsx +++ b/app/src/components/side-bar/settings/SettingsSideBarHeader.tsx @@ -6,6 +6,7 @@ import CircleIcon from "@components/CircleIcon"; import { useLogout } from "@features/authentication/logout/hooks/useLogout"; import { updateSideBarView } from "@state/side-bar/sideBar"; import { sideBarPages } from "types/sideBar"; +import { useSidebarType } from "../SideBarContext"; const StyledSideBarHeader = styled.div` height: 4rem !important; @@ -32,7 +33,13 @@ const IconsContainer = styled.div` `; function SettingsSideBarHeader() { - const { title } = useAppSelector((state) => state.sideBarData); + const type = useSidebarType(); + + const { title } = useAppSelector((state) => + type === "left" + ? state.sideBarData.leftSideBar + : state.sideBarData.rightSideBar + ); const { logout } = useLogout(); const dispatch = useAppDispatch(); @@ -53,7 +60,10 @@ function SettingsSideBarHeader() { $bgColor="var(--color-pattern)" onClick={() => dispatch( - updateSideBarView({ redirect: sideBarPages.PROFILE_UPDATE }) + updateSideBarView({ + redirect: sideBarPages.PROFILE_UPDATE, + data: { type }, + }) ) } /> diff --git a/app/src/components/side-bar/settings/side-bar-row/SideBarRow.tsx b/app/src/components/side-bar/settings/side-bar-row/SideBarRow.tsx index 284d270c..ce4d20ee 100644 --- a/app/src/components/side-bar/settings/side-bar-row/SideBarRow.tsx +++ b/app/src/components/side-bar/settings/side-bar-row/SideBarRow.tsx @@ -17,6 +17,7 @@ import { privacySettingsInterface, } from "types/user"; import { DataInterface, ExtractData } from "./getDataFactory"; +import { useSidebarType } from "@components/side-bar/SideBarContext"; const StyledSideBarRow = styled.div` height: 4rem; @@ -83,6 +84,7 @@ function SideBarRow({ redirect, icon, title, status, type }: SideBarRowProps) { const dataExtractor: DataInterface = ExtractData(type, currStatus, status); const data = dataExtractor.getData(); + const sideBarType = useSidebarType(); useEffect(() => { if (status !== undefined && type !== undefined) { @@ -95,7 +97,7 @@ function SideBarRow({ redirect, icon, title, status, type }: SideBarRowProps) { setCurrStatus( userData?.privacySettings?.[ key as keyof privacySettingsInterface - ] || "everyone", + ] || "everyone" ); break; case StatusType.ACTIVITY: @@ -103,7 +105,7 @@ function SideBarRow({ redirect, icon, title, status, type }: SideBarRowProps) { setCurrStatus( userData?.activitySettings?.[ key as keyof activitySettingsInterface - ] || "everyone", + ] || "everyone" ); break; case StatusType.PERMISSION: @@ -111,7 +113,7 @@ function SideBarRow({ redirect, icon, title, status, type }: SideBarRowProps) { setCurrStatus( userData?.permissionSettings?.[ key as keyof permissionsSettingsInterface - ] || "everyone", + ] || "everyone" ); break; default: @@ -127,7 +129,10 @@ function SideBarRow({ redirect, icon, title, status, type }: SideBarRowProps) { return ( - redirect && dispatch(updateSideBarView({ redirect, data })) + redirect && + dispatch( + updateSideBarView({ redirect, data: { ...data, type: sideBarType } }) + ) } data-testid={key && `menu-item-${key}`} > diff --git a/app/src/components/side-bar/settings/side-bar-row/getDataFactory.ts b/app/src/components/side-bar/settings/side-bar-row/getDataFactory.ts index 2617b509..c7f2a890 100644 --- a/app/src/components/side-bar/settings/side-bar-row/getDataFactory.ts +++ b/app/src/components/side-bar/settings/side-bar-row/getDataFactory.ts @@ -17,7 +17,7 @@ function ExtractData( | privacySettingsID | activitySettingsID | permissionSettingsID - | undefined, + | undefined ): DataInterface { switch (type) { case StatusType.PRIVACY: @@ -27,17 +27,17 @@ function ExtractData( case StatusType.PERMISSION: return getPermissionData(itemStatus as permissionSettingsID, currStatus); default: - return { getData: () => {} }; + return { getData: () => ({}) }; } } interface DataInterface { - getData: () => void; + getData: () => object; } function getPrivacyData( privacyStatus: privacySettingsID | undefined, - currStatus: string | undefined, + currStatus: string | undefined ): DataInterface { return { getData: () => { @@ -78,7 +78,7 @@ function getPrivacyData( function getActivityData( activityStatus: activitySettingsID | undefined, - currStatus: string | undefined, + currStatus: string | undefined ): DataInterface { return { getData: () => { @@ -119,7 +119,7 @@ function getActivityData( function getPermissionData( permissionStatus: permissionSettingsID | undefined, - currStatus: string | undefined, + currStatus: string | undefined ): DataInterface { return { getData: () => { diff --git a/app/src/features/chats/StartNewChat.tsx b/app/src/features/chats/StartNewChat.tsx index 7c833cef..85af66d2 100644 --- a/app/src/features/chats/StartNewChat.tsx +++ b/app/src/features/chats/StartNewChat.tsx @@ -8,6 +8,7 @@ import { useMouseLeave } from "@hooks/useMouseLeave"; import { useDispatch } from "react-redux"; import { updateSideBarView } from "@state/side-bar/sideBar"; import { sideBarPages } from "types/sideBar"; +import { useSidebarType } from "@components/side-bar/SideBarContext"; interface StyledListProps { $bottom?: number; @@ -52,6 +53,7 @@ function StartNewChat() { const [isMenuOpened, setIsMenuOpened] = useState(false); const ref = useMouseLeave(() => setIsMenuOpened(false), false); const dispatch = useDispatch(); + const sideBarType = useSidebarType(); const handleOpenMenu = () => { setIsMenuOpened((prevState) => !prevState); @@ -62,14 +64,14 @@ function StartNewChat() { dispatch( updateSideBarView({ redirect: sideBarPages.ADD_MEMBERS, - data: { type: "channel" }, + data: { type: sideBarType }, }) ); } else if (title === "New Group") { dispatch( updateSideBarView({ redirect: sideBarPages.ADD_MEMBERS, - data: { type: "group" }, + data: { type: sideBarType }, }) ); } else { diff --git a/app/src/features/chats/Topbar.tsx b/app/src/features/chats/Topbar.tsx index 08e9355d..6df08184 100644 --- a/app/src/features/chats/Topbar.tsx +++ b/app/src/features/chats/Topbar.tsx @@ -6,7 +6,7 @@ import Icon from "@components/Icon"; import SearchBar from "@features/search/components/SearchBar"; import PinnedMessages from "@features/pin-messages/components/PinnedMessages"; import { useAppDispatch, useAppSelector } from "@hooks/useGlobalState"; -import { useParams } from "react-router-dom"; +import { useOutletContext, useParams } from "react-router-dom"; import { getChatByID } from "./utils/helpers"; import { useChatMembers } from "./hooks/useChatMember"; import { getElapsedTime } from "@utils/helpers"; @@ -29,6 +29,7 @@ const Container = styled.div<{ $hasMargin?: boolean }>` align-items: center; padding-inline: 1rem; + cursor: pointer; margin: ${({ $hasMargin }) => ($hasMargin ? "1rem 0" : "0")}; `; @@ -102,6 +103,11 @@ const StyledButton = styled.button` color: var(--color-background); `; +interface OutletContextProps { + setShowRightSideBar: React.Dispatch>; + showRightSideBar: boolean; +} + //TODO: refactor function Topbar() { const { chatId } = useParams<{ chatId: string }>(); @@ -120,6 +126,9 @@ function Topbar() { const membersData = useChatMembers(chat?.members); const { removeFromBlockList } = useBlock(); + const { showRightSideBar, setShowRightSideBar } = + useOutletContext(); + let image; let lastSeen; @@ -159,7 +168,10 @@ function Topbar() { image={image} /> )} - + setShowRightSideBar(!showRightSideBar)} + > state.selectedUsers); const { createGroupOrChannel } = useSocket(); const onSubmit = (data: NewGroupForm) => { - dispatch(updateSideBarView({ redirect: sideBarPages.CHATS })); + dispatch( + updateSideBarView({ + redirect: sideBarPages.CHATS, + data: { type: sideBarType }, + }) + ); dispatch(clearSelectedUsers()); createGroupOrChannel({ diff --git a/app/src/features/privacy-settings/BlockList.tsx b/app/src/features/privacy-settings/BlockList.tsx index 35913faa..ed5d9ad1 100644 --- a/app/src/features/privacy-settings/BlockList.tsx +++ b/app/src/features/privacy-settings/BlockList.tsx @@ -6,6 +6,7 @@ import { useBlock } from "./hooks/useBlock"; import CircleIcon from "@components/CircleIcon"; import { useState } from "react"; import AddToBlockMenuList from "./AddToBlockMenuList"; +import { useSidebarType } from "@components/side-bar/SideBarContext"; const StyledOptionsList = styled.div` display: flex; @@ -30,8 +31,13 @@ export interface BlockListInterface { } function BlockList() { - const { props } = useAppSelector((state) => state.sideBarData); + const type = useSidebarType(); + const { props } = useAppSelector((state) => + type === "left" + ? state.sideBarData.leftSideBar + : state.sideBarData.rightSideBar + ); const { blockList } = useBlock(); const [isMenuOpened, setIsMenuOpened] = useState(false); diff --git a/app/src/features/profile-settings/ProfileSettings.tsx b/app/src/features/profile-settings/ProfileSettings.tsx index 36e3baf3..e24575c6 100644 --- a/app/src/features/profile-settings/ProfileSettings.tsx +++ b/app/src/features/profile-settings/ProfileSettings.tsx @@ -20,6 +20,7 @@ import { sideBarPages } from "types/sideBar"; import { useAppDispatch } from "@hooks/useGlobalState"; import { useUpdateProfilePicture } from "./hooks/useUpdateProfilePicture"; import { useDeleteProfilePicture } from "./hooks/useDeleteProfilePicture"; +import { useSidebarType } from "@components/side-bar/SideBarContext"; const SideBarContainer = styled.div` overflow-y: auto; @@ -177,10 +178,9 @@ export interface EditProfileForm { function ProfileSettings() { const { data: initialProfileSettings } = useProfileSettings(); const { updateProfileSettings, isPending } = useUpdateProfileSettings(); - const { deleteProfilePicture, isPending: isDeletingProfilePicture } = - useDeleteProfilePicture(); - const { updateProfilePicture, isPending: isUpdatingProfilePicture } = - useUpdateProfilePicture(); + const { deleteProfilePicture } = useDeleteProfilePicture(); + const { updateProfilePicture } = useUpdateProfilePicture(); + const sideBarType = useSidebarType(); const [selectedImageFile, setSelectedImageFile] = useState(null); const [hasImage, setHasImage] = useState( @@ -253,7 +253,7 @@ function ProfileSettings() { dispatch( updateSideBarView({ redirect: sideBarPages.SETTINGS, - data: undefined, + data: { type: sideBarType }, }) ); } diff --git a/app/src/state/side-bar/sideBar.ts b/app/src/state/side-bar/sideBar.ts index cb82817f..e8dfbe9d 100644 --- a/app/src/state/side-bar/sideBar.ts +++ b/app/src/state/side-bar/sideBar.ts @@ -21,7 +21,16 @@ interface SideBarView { backView?: sideBarPages; props?: object; } -const initialState: SideBarView = chats; + +interface SideBarState { + leftSideBar: SideBarView; + rightSideBar: SideBarView; +} + +const initialState: SideBarState = { + leftSideBar: chats, + rightSideBar: settings, +}; function getSideBarPage(type: number): SideBarView { switch (type) { @@ -54,7 +63,7 @@ function getSideBarPage(type: number): SideBarView { interface actionType { redirect: sideBarPages; - data?: { [key: string]: string }; + data: { type: "left" | "right"; [key: string]: string }; } const sideBarSlice = createSlice({ @@ -64,27 +73,20 @@ const sideBarSlice = createSlice({ updateSideBarView: (state, action: PayloadAction) => { const { redirect, data } = action.payload; - // redirect can take value 0, so don't try "if(redirect)" - if (redirect === undefined) return; - - // state assigned one by one to avoid state de-serialization + const whichSide = data.type; const newData = getSideBarPage(redirect); - state.backView = newData.backView; - state.page = newData.page; - if (redirect === sideBarPages.SETTINGS_UPDATE) { - state.props = { data: data }; - state.title = data?.header || newData.title; - } else if ( - redirect === sideBarPages.BLOCKED_USERS || - redirect === sideBarPages.ADD_MEMBERS || - redirect === sideBarPages.NEW_GROUP - ) { - state.props = { ...newData.props, data: data }; - state.title = newData.title; + const updatedSideBar = { + page: newData.page, + title: newData.title, + backView: newData.backView, + props: { ...newData.props }, + }; + + if (whichSide === "left") { + state.leftSideBar = updatedSideBar; } else { - state.title = newData.title; - state.props = { ...newData.props }; + state.rightSideBar = updatedSideBar; } }, },