Add modal state provider (#125)

This commit is contained in:
Szymon Szlachtowicz 2021-11-12 12:33:56 +01:00 committed by GitHub
parent 03ea71dca4
commit 3be3a7726f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 159 additions and 104 deletions

View File

@ -19,17 +19,11 @@ export function Chat() {
const narrow = useNarrow(); const narrow = useNarrow();
const [isModalVisible, setIsModalVisible] = useState(false);
const showModal = () => setIsModalVisible(true);
const [isEditVisible, setIsEditVisible] = useState(false);
const showEditModal = () => setIsEditVisible(true);
return ( return (
<ChatWrapper> <ChatWrapper>
{!narrow && ( {!narrow && (
<ChannelsWrapper> <ChannelsWrapper>
<StyledCommunity onClick={showModal} /> <StyledCommunity />
<Channels <Channels
membersList={membersList} membersList={membersList}
groupList={groupList} groupList={groupList}
@ -42,8 +36,6 @@ export function Chat() {
<ChatBody <ChatBody
onClick={() => setShowMembers(!showMembers)} onClick={() => setShowMembers(!showMembers)}
showMembers={showMembers} showMembers={showMembers}
onCommunityClick={showModal}
onEditClick={showEditModal}
membersList={membersList} membersList={membersList}
groupList={groupList} groupList={groupList}
setMembersList={setMembersList} setMembersList={setMembersList}
@ -61,15 +53,8 @@ export function Chat() {
setCreateChat={setCreateChat} setCreateChat={setCreateChat}
/> />
)} )}
<CommunityModal <CommunityModal subtitle="Public Community" />
isVisible={isModalVisible} <EditModal />
onClose={() => setIsModalVisible(false)}
subtitle="Public Community"
/>
<EditModal
isVisible={isEditVisible}
onClose={() => setIsEditVisible(false)}
/>
</ChatWrapper> </ChatWrapper>
); );
} }

View File

@ -27,8 +27,6 @@ enum ChatBodyState {
interface ChatBodyProps { interface ChatBodyProps {
onClick: () => void; onClick: () => void;
showMembers: boolean; showMembers: boolean;
onCommunityClick: () => void;
onEditClick: () => void;
membersList: string[]; membersList: string[];
groupList: [][]; groupList: [][];
setMembersList: any; setMembersList: any;
@ -39,8 +37,6 @@ interface ChatBodyProps {
export function ChatBody({ export function ChatBody({
onClick, onClick,
showMembers, showMembers,
onCommunityClick,
onEditClick,
membersList, membersList,
groupList, groupList,
setMembersList, setMembersList,
@ -88,7 +84,7 @@ export function ChatBody({
<> <>
{narrow && ( {narrow && (
<CommunityWrap className={className}> <CommunityWrap className={className}>
<Community onClick={onCommunityClick} /> <Community />
</CommunityWrap> </CommunityWrap>
)} )}
@ -113,7 +109,7 @@ export function ChatBody({
{!narrow && ( {!narrow && (
<MemberBtn <MemberBtn
onClick={onClick} onClick={onClick}
className={showMembers && !narrow ? "active" : ""} className={showMembers ? "active" : ""}
> >
<MembersIcon /> <MembersIcon />
</MemberBtn> </MemberBtn>
@ -129,7 +125,6 @@ export function ChatBody({
switchMemberList={() => switchShowState(ChatBodyState.Members)} switchMemberList={() => switchShowState(ChatBodyState.Members)}
setShowChannelMenu={setShowChannelMenu} setShowChannelMenu={setShowChannelMenu}
setEditGroup={setEditGroup} setEditGroup={setEditGroup}
onEditClick={onEditClick}
setGroupList={setGroupList} setGroupList={setGroupList}
/> />
)} )}

View File

@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
import styled, { useTheme } from "styled-components"; import styled, { useTheme } from "styled-components";
import { useMessengerContext } from "../../contexts/messengerProvider"; import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider";
import { useLow } from "../../contexts/narrowProvider"; import { useLow } from "../../contexts/narrowProvider";
import { lightTheme, Theme } from "../../styles/themes"; import { lightTheme, Theme } from "../../styles/themes";
import { uintToImgUrl } from "../../utils/uintToImgUrl"; import { uintToImgUrl } from "../../utils/uintToImgUrl";
@ -11,7 +12,7 @@ import { GifIcon } from "../Icons/GifIcon";
import { PictureIcon } from "../Icons/PictureIcon"; import { PictureIcon } from "../Icons/PictureIcon";
import { StickerIcon } from "../Icons/StickerIcon"; import { StickerIcon } from "../Icons/StickerIcon";
import "emoji-mart/css/emoji-mart.css"; import "emoji-mart/css/emoji-mart.css";
import { Modal } from "../Modals/Modal"; import { SizeLimitModal, SizeLimitModalName } from "../Modals/SizeLimitModal";
export function ChatInput() { export function ChatInput() {
const { sendMessage } = useMessengerContext(); const { sendMessage } = useMessengerContext();
@ -20,7 +21,6 @@ export function ChatInput() {
const [showEmoji, setShowEmoji] = useState(false); const [showEmoji, setShowEmoji] = useState(false);
const [inputHeight, setInputHeight] = useState(40); const [inputHeight, setInputHeight] = useState(40);
const [imageUint, setImageUint] = useState<undefined | Uint8Array>(undefined); const [imageUint, setImageUint] = useState<undefined | Uint8Array>(undefined);
const [showSizeLimit, setShowSizeLimit] = useState(false);
useEffect(() => { useEffect(() => {
window.addEventListener("click", () => setShowEmoji(false)); window.addEventListener("click", () => setShowEmoji(false));
@ -69,16 +69,11 @@ export function ChatInput() {
const low = useLow(); const low = useLow();
const { setModal } = useModal(SizeLimitModalName);
return ( return (
<View> <View>
<Modal onClose={() => setShowSizeLimit(false)} isVisible={showSizeLimit}> <SizeLimitModal />
<div
onClick={() => setShowSizeLimit(false)}
style={{ padding: "20px" }}
>
File size must be less than 1MB
</div>
</Modal>
{showEmoji && ( {showEmoji && (
<div> <div>
<Picker <Picker
@ -119,7 +114,7 @@ export function ChatInput() {
if (e.target.files[0].size < 1024 * 1024) { if (e.target.files[0].size < 1024 * 1024) {
fileReader.readAsArrayBuffer(e.target.files[0]); fileReader.readAsArrayBuffer(e.target.files[0]);
} else { } else {
setShowSizeLimit(true); setModal(true);
} }
} }
}} }}

View File

@ -1,8 +1,9 @@
import React, { useMemo, useRef, useState } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useBlockedUsers } from "../../contexts/blockedUsersProvider"; import { useBlockedUsers } from "../../contexts/blockedUsersProvider";
import { useMessengerContext } from "../../contexts/messengerProvider"; import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider";
import { useChatScrollHandle } from "../../hooks/useChatScrollHandle"; import { useChatScrollHandle } from "../../hooks/useChatScrollHandle";
import { ChatMessage } from "../../models/ChatMessage"; import { ChatMessage } from "../../models/ChatMessage";
import { equalDate } from "../../utils"; import { equalDate } from "../../utils";
@ -10,8 +11,8 @@ import { EmptyChannel } from "../Channels/EmptyChannel";
import { ContactMenu } from "../Form/ContactMenu"; import { ContactMenu } from "../Form/ContactMenu";
import { LoadingIcon } from "../Icons/LoadingIcon"; import { LoadingIcon } from "../Icons/LoadingIcon";
import { UserIcon } from "../Icons/UserIcon"; import { UserIcon } from "../Icons/UserIcon";
import { LinkModal } from "../Modals/LinkModal"; import { LinkModal, LinkModalName } from "../Modals/LinkModal";
import { PictureModal } from "../Modals/PictureModal"; import { PictureModal, PictureModalName } from "../Modals/PictureModal";
import { textSmallStyles } from "../Text"; import { textSmallStyles } from "../Text";
import { ChatMessageContent } from "./ChatMessageContent"; import { ChatMessageContent } from "./ChatMessageContent";
@ -84,14 +85,17 @@ export function ChatMessages() {
const [image, setImage] = useState(""); const [image, setImage] = useState("");
const [link, setLink] = useState(""); const [link, setLink] = useState("");
const { setModal: setPictureModal } = useModal(PictureModalName);
const { setModal: setLinkModal } = useModal(LinkModalName);
useEffect(() => (!image ? undefined : setPictureModal(true)), [image]);
useEffect(() => (!link ? undefined : setLinkModal(true)), [link]);
return ( return (
<MessagesWrapper ref={ref}> <MessagesWrapper ref={ref}>
<PictureModal <PictureModal image={image} />
isVisible={!!image} <LinkModal link={link} />
onClose={() => setImage("")}
image={image}
/>
<LinkModal isVisible={!!link} onClose={() => setLink("")} link={link} />
<EmptyChannel channel={activeChannel} /> <EmptyChannel channel={activeChannel} />
{loadingMessages && ( {loadingMessages && (
<LoadingWrapper> <LoadingWrapper>

View File

@ -2,17 +2,19 @@ import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useMessengerContext } from "../contexts/messengerProvider"; import { useMessengerContext } from "../contexts/messengerProvider";
import { useModal } from "../contexts/modalProvider";
import { CommunityIdentity } from "./CommunityIdentity"; import { CommunityIdentity } from "./CommunityIdentity";
import { CommunityModalName } from "./Modals/CommunityModal";
import { CommunitySkeleton } from "./Skeleton/CommunitySkeleton"; import { CommunitySkeleton } from "./Skeleton/CommunitySkeleton";
interface CommunityProps { interface CommunityProps {
onClick: () => void;
className?: string; className?: string;
} }
export function Community({ onClick, className }: CommunityProps) { export function Community({ className }: CommunityProps) {
const { communityData } = useMessengerContext(); const { communityData } = useMessengerContext();
const { setModal } = useModal(CommunityModalName);
if (!communityData) { if (!communityData) {
return ( return (
@ -24,7 +26,7 @@ export function Community({ onClick, className }: CommunityProps) {
return ( return (
<> <>
<button className={className} onClick={onClick}> <button className={className} onClick={() => setModal(true)}>
<CommunityIdentity subtitle={`${communityData.members} members`} /> <CommunityIdentity subtitle={`${communityData.members} members`} />
</button> </button>
</> </>

View File

@ -2,6 +2,7 @@ import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useMessengerContext } from "../../contexts/messengerProvider"; import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider";
import { useNarrow } from "../../contexts/narrowProvider"; import { useNarrow } from "../../contexts/narrowProvider";
import { ChannelData } from "../../models/ChannelData"; import { ChannelData } from "../../models/ChannelData";
import { AddMemberIconSvg } from "../Icons/AddMemberIcon"; import { AddMemberIconSvg } from "../Icons/AddMemberIcon";
@ -10,6 +11,7 @@ import { EgitGroupSvg } from "../Icons/EditGroupIcon";
import { LeftIconSvg } from "../Icons/LeftIcon"; import { LeftIconSvg } from "../Icons/LeftIcon";
import { MembersSmallSvg } from "../Icons/MembersSmallIcon"; import { MembersSmallSvg } from "../Icons/MembersSmallIcon";
import { MuteSvg } from "../Icons/MuteIcon"; import { MuteSvg } from "../Icons/MuteIcon";
import { EditModalName } from "../Modals/EditModal";
import { DropdownMenu, MenuItem, MenuText } from "./DropdownMenu"; import { DropdownMenu, MenuItem, MenuText } from "./DropdownMenu";
@ -18,7 +20,6 @@ interface ChannelMenuProps {
switchMemberList: () => void; switchMemberList: () => void;
setShowChannelMenu: (val: boolean) => void; setShowChannelMenu: (val: boolean) => void;
setEditGroup: (val: boolean) => void; setEditGroup: (val: boolean) => void;
onEditClick: () => void;
setGroupList: any; setGroupList: any;
} }
@ -27,13 +28,12 @@ export const ChannelMenu = ({
switchMemberList, switchMemberList,
setShowChannelMenu, setShowChannelMenu,
setEditGroup, setEditGroup,
onEditClick,
setGroupList, setGroupList,
}: ChannelMenuProps) => { }: ChannelMenuProps) => {
const narrow = useNarrow(); const narrow = useNarrow();
const { clearNotifications, setActiveChannel, channels } = const { clearNotifications, setActiveChannel, channels } =
useMessengerContext(); useMessengerContext();
const { setModal } = useModal(EditModalName);
return ( return (
<DropdownMenu> <DropdownMenu>
{narrow && ( {narrow && (
@ -58,7 +58,7 @@ export const ChannelMenu = ({
<AddMemberIconSvg width={16} height={16} /> <AddMemberIconSvg width={16} height={16} />
<MenuText>Add / remove from group</MenuText> <MenuText>Add / remove from group</MenuText>
</MenuItem> </MenuItem>
<MenuItem onClick={onEditClick}> <MenuItem onClick={() => setModal(true)}>
<EgitGroupSvg width={16} height={16} /> <EgitGroupSvg width={16} height={16} />
<MenuText>Edit name and image</MenuText> <MenuText>Edit name and image</MenuText>
</MenuItem> </MenuItem>

View File

@ -12,20 +12,18 @@ import { CopyInput } from "../Form/CopyInput";
import { StatusLogo } from "../Icons/StatusLogo"; import { StatusLogo } from "../Icons/StatusLogo";
import { textSmallStyles } from "../Text"; import { textSmallStyles } from "../Text";
import { BasicModalProps, Modal } from "./Modal"; import { Modal } from "./Modal";
import { Section, Text } from "./ModalStyle"; import { Section, Text } from "./ModalStyle";
interface CommunityModalProps extends BasicModalProps, CommunityIdentityProps {} export const CommunityModalName = "CommunityModal";
export const CommunityModal = ({ type CommunityModalProps = CommunityIdentityProps;
isVisible,
onClose, export const CommunityModal = ({ subtitle }: CommunityModalProps) => {
subtitle,
}: CommunityModalProps) => {
const narrow = useNarrow(); const narrow = useNarrow();
const { communityData } = useMessengerContext(); const { communityData } = useMessengerContext();
return ( return (
<Modal isVisible={isVisible} onClose={onClose}> <Modal name={CommunityModalName}>
<Section> <Section>
<CommunityIdentity subtitle={subtitle} /> <CommunityIdentity subtitle={subtitle} />
</Section> </Section>

View File

@ -2,15 +2,18 @@ import React, { useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useMessengerContext } from "../../contexts/messengerProvider"; import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider";
import { buttonStyles } from "../Buttons/buttonStyle"; import { buttonStyles } from "../Buttons/buttonStyle";
import { ChannelLogo } from "../Channels/Channel"; import { ChannelLogo } from "../Channels/Channel";
import { AddIcon } from "../Icons/AddIcon"; import { AddIcon } from "../Icons/AddIcon";
import { textMediumStyles } from "../Text"; import { textMediumStyles } from "../Text";
import { BasicModalProps, Modal } from "./Modal"; import { Modal } from "./Modal";
import { Heading, Section } from "./ModalStyle"; import { Heading, Section } from "./ModalStyle";
export const EditModal = ({ isVisible, onClose }: BasicModalProps) => { export const EditModalName = "editModal";
export const EditModal = () => {
const { activeChannel } = useMessengerContext(); const { activeChannel } = useMessengerContext();
const [groupName, setGroupName] = useState(""); const [groupName, setGroupName] = useState("");
const [image, setImage] = useState(""); const [image, setImage] = useState("");
@ -21,14 +24,16 @@ export const EditModal = ({ isVisible, onClose }: BasicModalProps) => {
} }
}; };
const { setModal } = useModal(EditModalName);
const handleUpload = () => { const handleUpload = () => {
activeChannel.icon = image; activeChannel.icon = image;
activeChannel.name = groupName; activeChannel.name = groupName;
onClose(); setModal(false);
}; };
return ( return (
<Modal isVisible={isVisible} onClose={onClose}> <Modal name={EditModalName}>
<Section> <Section>
<Heading>Edit group name and image</Heading> <Heading>Edit group name and image</Heading>
</Section> </Section>

View File

@ -1,19 +1,23 @@
import React from "react"; import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useModal } from "../../contexts/modalProvider";
import { buttonStyles } from "../Buttons/buttonStyle"; import { buttonStyles } from "../Buttons/buttonStyle";
import { textMediumStyles } from "../Text"; import { textMediumStyles } from "../Text";
import { BasicModalProps, Modal } from "./Modal"; import { Modal } from "./Modal";
import { Heading, Section } from "./ModalStyle"; import { Heading, Section } from "./ModalStyle";
interface LinkModalProps extends BasicModalProps { export const LinkModalName = "LinkModal";
interface LinkModalProps {
link: string; link: string;
} }
export const LinkModal = ({ isVisible, onClose, link }: LinkModalProps) => { export const LinkModal = ({ link }: LinkModalProps) => {
const { setModal } = useModal(LinkModalName);
return ( return (
<Modal isVisible={isVisible} onClose={onClose}> <Modal name={LinkModalName}>
<Section> <Section>
<Heading>Are you sure you want to visit this website?</Heading> <Heading>Are you sure you want to visit this website?</Heading>
</Section> </Section>
@ -21,11 +25,11 @@ export const LinkModal = ({ isVisible, onClose, link }: LinkModalProps) => {
<Link>{link}</Link> <Link>{link}</Link>
</Section> </Section>
<ButtonSection> <ButtonSection>
<ButtonNo onClick={onClose}>No</ButtonNo> <ButtonNo onClick={() => setModal(false)}>No</ButtonNo>
<ButtonYes <ButtonYes
onClick={() => { onClick={() => {
window?.open(link, "_blank", "noopener")?.focus(); window?.open(link, "_blank", "noopener")?.focus();
onClose(); setModal(false);
}} }}
> >
Yes, take me there Yes, take me there

View File

@ -2,11 +2,11 @@ import React, { ReactNode, useCallback, useEffect } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import styled from "styled-components"; import styled from "styled-components";
import { useModal } from "../../contexts/modalProvider";
import { CrossIcon } from "../Icons/CrossIcon"; import { CrossIcon } from "../Icons/CrossIcon";
export interface BasicModalProps { export interface BasicModalProps {
isVisible: boolean; name: string;
onClose: () => void;
className?: string; className?: string;
} }
@ -14,19 +14,16 @@ export interface ModalProps extends BasicModalProps {
children: ReactNode; children: ReactNode;
} }
export const Modal = ({ export const Modal = ({ name, children, className }: ModalProps) => {
isVisible, const { isVisible, setModal } = useModal(name);
onClose,
children,
className,
}: ModalProps) => {
const listenKeyboard = useCallback( const listenKeyboard = useCallback(
(event: KeyboardEvent) => { (event: KeyboardEvent) => {
if (event.key === "Escape" || event.keyCode === 27) { if (event.key === "Escape" || event.keyCode === 27) {
onClose(); setModal(false);
} }
}, },
[onClose] [setModal]
); );
useEffect(() => { useEffect(() => {
@ -40,18 +37,23 @@ export const Modal = ({
if (!isVisible) return null; if (!isVisible) return null;
return createPortal( const element = document.getElementById("modal-root");
<ModalView>
<ModalOverlay onClick={onClose} /> if (element) {
<ModalBody className={className}> return createPortal(
<CloseButton onClick={onClose} className={className}> <ModalView>
<CrossIcon /> <ModalOverlay onClick={() => setModal(false)} />
</CloseButton> <ModalBody className={className}>
{children} <CloseButton onClick={() => setModal(false)} className={className}>
</ModalBody> <CrossIcon />
</ModalView>, </CloseButton>
document.body {children}
); </ModalBody>
</ModalView>,
element
);
}
return null;
}; };
const ModalView = styled.div` const ModalView = styled.div`

View File

@ -1,19 +1,17 @@
import React from "react"; import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { BasicModalProps, Modal } from "./Modal"; import { Modal } from "./Modal";
interface PictureModalProps extends BasicModalProps { export const PictureModalName = "PictureModal";
interface PictureModalProps {
image: string; image: string;
} }
export const PictureModal = ({ export const PictureModal = ({ image }: PictureModalProps) => {
isVisible,
onClose,
image,
}: PictureModalProps) => {
return ( return (
<Modal isVisible={isVisible} onClose={onClose} className="picture"> <Modal name={PictureModalName} className="picture">
<ModalImageWrapper> <ModalImageWrapper>
<ModalImage src={image}></ModalImage> <ModalImage src={image}></ModalImage>
</ModalImageWrapper> </ModalImageWrapper>

View File

@ -0,0 +1,19 @@
import React from "react";
import { useModal } from "../../contexts/modalProvider";
import { Modal } from "./Modal";
export const SizeLimitModalName = "SizeLimitModal";
export function SizeLimitModal() {
const { setModal } = useModal(SizeLimitModalName);
return (
<Modal name={SizeLimitModalName}>
<div onClick={() => setModal(false)} style={{ padding: "20px" }}>
File size must be less than 1MB
</div>
</Modal>
);
}

View File

@ -4,6 +4,7 @@ import styled from "styled-components";
import { BlockedUsersProvider } from "../contexts/blockedUsersProvider"; import { BlockedUsersProvider } from "../contexts/blockedUsersProvider";
import { FetchMetadataProvider } from "../contexts/fetchMetadataProvider"; import { FetchMetadataProvider } from "../contexts/fetchMetadataProvider";
import { ModalProvider } from "../contexts/modalProvider";
import { NarrowProvider } from "../contexts/narrowProvider"; import { NarrowProvider } from "../contexts/narrowProvider";
import { Metadata } from "../models/Metadata"; import { Metadata } from "../models/Metadata";
import { GlobalStyle } from "../styles/GlobalStyle"; import { GlobalStyle } from "../styles/GlobalStyle";
@ -28,10 +29,13 @@ export function ReactChat({
<NarrowProvider myRef={ref}> <NarrowProvider myRef={ref}>
<FetchMetadataProvider fetchMetadata={fetchMetadata}> <FetchMetadataProvider fetchMetadata={fetchMetadata}>
<BlockedUsersProvider> <BlockedUsersProvider>
<Wrapper ref={ref}> <ModalProvider>
<GlobalStyle /> <Wrapper ref={ref}>
<ChatLoader communityKey={communityKey} /> <GlobalStyle />
</Wrapper> <ChatLoader communityKey={communityKey} />
<div id="modal-root" />
</Wrapper>
</ModalProvider>
</BlockedUsersProvider> </BlockedUsersProvider>
</FetchMetadataProvider> </FetchMetadataProvider>
</NarrowProvider> </NarrowProvider>

View File

@ -0,0 +1,44 @@
import React, {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from "react";
type ModalsState = {
[name: string]: boolean;
};
type ModalContextType = [
state: ModalsState,
setState: React.Dispatch<React.SetStateAction<ModalsState>>
];
const ModalContext = createContext<ModalContextType>([{}, () => undefined]);
export function useModal(name: string) {
const [modals, setModals] = useContext(ModalContext);
const setModal = useCallback(
(state: boolean) => {
setModals((prev) => {
return {
...prev,
[name]: state,
};
});
},
[name, modals]
);
const isVisible = useMemo(() => modals?.[name] ?? false, [modals, name]);
return { isVisible, setModal };
}
interface IdentityProviderProps {
children: React.ReactNode;
}
export function ModalProvider({ children }: IdentityProviderProps) {
const modalState = useState<ModalsState>({});
return <ModalContext.Provider value={modalState} children={children} />;
}