diff --git a/package.json b/package.json index 788af56f9a..cce645b5da 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,9 @@ "@mui/lab": "^5.0.0-alpha.72", "@mui/material": "^5.5.0", "@pushprotocol/ledgerlive": "latest", - "@pushprotocol/restapi": "1.4.21", + "@pushprotocol/restapi": "0.0.1-alpha.48", "@pushprotocol/socket": "latest", - "@pushprotocol/uiweb": "1.1.14", + "@pushprotocol/uiweb": "0.0.1-alpha.17", "@reduxjs/toolkit": "^1.7.1", "@testing-library/dom": "^9.0.1", "@testing-library/jest-dom": "^4.2.4", @@ -101,9 +101,10 @@ "react-dropzone-uploader": "2.11.0", "react-easy-crop": "^4.1.4", "react-ga": "2.7.0", - "react-icons": "^4.7.1", + "react-icons": "^4.11.0", "react-image-file-resizer": "^0.4.7", "react-images-upload": "^1.2.8", + "react-input-slider": "^6.0.1", "react-joyride": "^2.4.0", "react-loader-spinner": "^5.3.4", "react-multi-select-component": "^4.2.3", diff --git a/public/svg/manageSettings.svg b/public/svg/manageSettings.svg new file mode 100644 index 0000000000..5d6138abd1 --- /dev/null +++ b/public/svg/manageSettings.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/svg/optout.svg b/public/svg/optout.svg new file mode 100644 index 0000000000..632a2fa47d --- /dev/null +++ b/public/svg/optout.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/svg/setting.svg b/public/svg/setting.svg new file mode 100644 index 0000000000..ac18686681 --- /dev/null +++ b/public/svg/setting.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/components/ChannelDetails.js b/src/components/ChannelDetails.js index 5378c6e53a..ef19384ad4 100644 --- a/src/components/ChannelDetails.js +++ b/src/components/ChannelDetails.js @@ -1,46 +1,57 @@ // React + Web3 Essentials import { ethers } from 'ethers'; -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; // External Packages import moment from 'moment'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; +import { useNavigate } from 'react-router-dom'; +import { MdRemoveCircleOutline } from 'react-icons/md'; // Internal Compoonents -import { getReq, postReq } from 'api'; -import { ButtonV2, ImageV2, ItemHV2, ItemVV2, SpanV2 } from "components/reusables/SharedStylingV2"; -import { convertAddressToAddrCaip } from 'helpers/CaipHelper'; +import { ButtonV2, ImageV2, ItemHV2, ItemVV2, SpanV2 } from 'components/reusables/SharedStylingV2'; import { useAccount, useDeviceWidthCheck } from 'hooks'; import ChannelsDataStore from 'singletons/ChannelsDataStore'; import ChannelSettings from './ChannelSettings'; -import ShowDelegates from './ShowDelegates'; +import { Section } from 'primaries/SharedStyling'; +import useModalBlur, { MODAL_POSITION } from 'hooks/useModalBlur'; +import useToast from 'hooks/useToast'; +import AddDelegateModalContent from './AddDelegateModalContent'; +import { AddDelegateButton, ManageSettingsButton } from './channel/ChannelButtons'; +import { Button, Item } from 'components/SharedStyling'; +import ChannelInfoHeader from './channel/ChannelInfoHeader'; +import ChannelInfoList from './channel/ChannelInfoList'; +import RedCircleSvg from '../assets/RedCircle.svg'; // Internal Configs -import { appConfig } from "config"; -import { device } from "config/Globals"; +import { appConfig } from 'config'; +import { device } from 'config/Globals'; import { CHANNEL_TYPE } from 'helpers/UtilityHelper'; import { getDateFromTimestamp, nextDaysDateFromTimestamp, timeRemaining } from 'helpers/TimerHelper'; -import RedCircleSvg from "../assets/RedCircle.svg" -import { Button } from "components/SharedStyling"; +import APP_PATHS from 'config/AppPaths'; +import { convertAddressToAddrCaip } from 'helpers/CaipHelper'; +import { getChannelDelegates } from 'services'; + const DATE_FORMAT = 'DD MMM, YYYY'; -export default function ChannelDetails({ isChannelExpired, setIsChannelExpired, showEditChannel, destroyChannel -}) { - const { chainId } = useAccount(); +export default function ChannelDetails({ isChannelExpired, setIsChannelExpired, showEditChannel, destroyChannel }) { + const { account, chainId } = useAccount(); const { + delegatees, channelDetails, canVerify, - aliasDetails: { isAliasVerified, aliasAddrFromContract } + aliasDetails: { isAliasVerified, aliasAddrFromContract }, } = useSelector((state) => state.admin); + const { channelSettings } = useSelector((state) => state.channels); const { CHANNEL_ACTIVE_STATE, CHANNNEL_DEACTIVATED_STATE } = useSelector((state) => state.channels); const { processingState } = useSelector((state) => state.channelCreation); const [verifyingChannel, setVerifyingChannel] = React.useState([]); const [creationDate, setCreationDate] = React.useState(''); let { channelState } = channelDetails; - if(!channelState) channelState = channelDetails['activation_status']; + if (!channelState) channelState = channelDetails['activation_status']; const channelIsActive = channelState === CHANNEL_ACTIVE_STATE; const channelIsDeactivated = channelState === CHANNNEL_DEACTIVATED_STATE; @@ -48,21 +59,44 @@ export default function ChannelDetails({ isChannelExpired, setIsChannelExpired, const onCoreNetwork = CORE_CHAIN_ID === chainId; const isMobile = useDeviceWidthCheck(600); + const [delegateeList, setDelegateeList] = React.useState([account]); + const [channelAddress, setChannelAddress] = React.useState(undefined); + const { epnsCommWriteProvider } = useSelector((state) => state.contracts); + + const navigate = useNavigate(); + + const { + isModalOpen: isAddDelegateModalOpen, + showModal: showAddDelegateModal, + ModalComponent: AddDelegateModalComponent, + } = useModalBlur(); + + const addDelegateToast = useToast(); + const addDelegate = async (walletAddress) => { + return epnsCommWriteProvider.addDelegate(walletAddress); + }; + // BEGIN CHANGE // Added this inline if-else condition because of a bug that when connecting to Mumbai, the channelDetails.expiryType is undefined, so the toString() is throwing an exception - const channelExpiryDate = channelDetails.expiryTime ? getDateFromTimestamp(channelDetails.expiryTime?.toString() * 1000) : '' - const isChannelNotExpired = channelDetails.expiryTime ? timeRemaining(channelDetails.expiryTime?.toString() * 1000) : true - const channelAutomaticExpiryDate = channelDetails.expiryTime ? nextDaysDateFromTimestamp(channelDetails.expiryTime?.toString() * 1000, 14) : '' + const channelExpiryDate = channelDetails.expiryTime + ? getDateFromTimestamp(channelDetails.expiryTime?.toString() * 1000) + : ''; + const isChannelNotExpired = channelDetails.expiryTime + ? timeRemaining(channelDetails.expiryTime?.toString() * 1000) + : true; + const channelAutomaticExpiryDate = channelDetails.expiryTime + ? nextDaysDateFromTimestamp(channelDetails.expiryTime?.toString() * 1000, 14) + : ''; // END CHANGE useEffect(() => { - if(channelDetails.channelType != CHANNEL_TYPE["TIMEBOUND"]) return; - if(!isChannelNotExpired) setIsChannelExpired(true); + if (channelDetails.channelType != CHANNEL_TYPE['TIMEBOUND']) return; + if (!isChannelNotExpired) setIsChannelExpired(true); }, [isChannelNotExpired]); React.useEffect(() => { if (!channelDetails || !canVerify) return; - (async function() { + (async function () { let channelJson = await ChannelsDataStore.instance.getChannelJsonAsync(channelDetails.verifiedBy); setVerifyingChannel(channelJson); })(); @@ -70,7 +104,7 @@ export default function ChannelDetails({ isChannelExpired, setIsChannelExpired, React.useEffect(() => { if (!channelDetails || !onCoreNetwork) return; - (async function() { + (async function () { const bn = channelDetails.channelStartBlock.toString(); // using ethers jsonRpcProvider instead of library bcz channels are created on only core chain, that's why block can be fetched from that only @@ -80,45 +114,134 @@ export default function ChannelDetails({ isChannelExpired, setIsChannelExpired, })(); }, [channelDetails]); + React.useEffect(() => { + if (!account) return; + if (!delegatees || !delegatees.length) { + setChannelAddress(account); + } else { + // default the channel address to the first one on the list which should be that of the user if they have a channel + if (onCoreNetwork) setChannelAddress(delegatees[0].channel); + else setChannelAddress(delegatees[0].alias_address); + } + }, [delegatees, account]); + + useEffect(() => { + if (account) { + (async () => { + try { + const channelAddressinCAIP = convertAddressToAddrCaip(account, chainId); + const channelDelegates = await getChannelDelegates({ channelCaipAddress: channelAddressinCAIP }); + if (channelDelegates) { + const delegateeList = channelDelegates.map((delegate) => delegate); + delegateeList.unshift(account); + setDelegateeList(delegateeList); + } + } catch (err) { + console.error(err); + } + })(); + } + }, [account]); + + const removeDelegate = (walletAddress) => { + return epnsCommWriteProvider.removeDelegate(walletAddress); + }; + + const navigateToNotifSettings = () => { + navigate(APP_PATHS.ChannelSettings); + }; return ( - + - + {channelDetails.name} {canVerify && } - + {(onCoreNetwork && aliasAddrFromContract && !isAliasVerified) || (!onCoreNetwork && !isAliasVerified) ? ( Alias Network Setup Pending ) : ( - subscount + subscount {channelDetails.subscriber_count} - {channelIsDeactivated && } + {channelIsDeactivated && ( + + )} {channelIsActive ? 'Active' : channelIsDeactivated ? 'Deactivated' : 'Blocked'} - { - channelDetails.channelType == CHANNEL_TYPE["TIMEBOUND"] && !isChannelExpired && - - - Expires on {channelExpiryDate} - - } - { - channelDetails.channelType == CHANNEL_TYPE["TIMEBOUND"] && isChannelExpired && - - - Expired on {channelExpiryDate} - - } + {channelDetails.channelType == CHANNEL_TYPE['TIMEBOUND'] && !isChannelExpired && ( + + + + Expires on {channelExpiryDate} + + + )} + {channelDetails.channelType == CHANNEL_TYPE['TIMEBOUND'] && isChannelExpired && ( + + + + Expired on {channelExpiryDate} + + + )} )} @@ -127,40 +250,49 @@ export default function ChannelDetails({ isChannelExpired, setIsChannelExpired, - {isMobile && - + {isMobile && ( + {!isChannelExpired && onCoreNetwork && Edit Channel} {!isChannelExpired && } - {isChannelExpired && onCoreNetwork && - Delete Channel - } + )} - } + )} - {isChannelExpired && + {isChannelExpired && ( - - Note:{" "} - Channel will auto delete on {" "} - {channelAutomaticExpiryDate} - + + Note: Channel will auto delete on{' '} + {channelAutomaticExpiryDate} + - } + )} - {channelDetails.info} - - {canVerify && - + + {channelDetails.info} + + + {canVerify && ( + verified by: @@ -169,41 +301,101 @@ export default function ChannelDetails({ isChannelExpired, setIsChannelExpired, - } - - {processingState === 0 && + )} + + {processingState === 0 && ( - +
+ + } + /> + + +
+
+ + showAddDelegateModal()} />} + /> + , + }, + ]} + /> + +
- } + )} + {/* modal to add a delegate */} +
); } const AdaptiveMobileItemVV2 = styled(ItemVV2)` - @media (max-width: 767px) { align-items: center; } -` +`; const DestroyChannelBtn = styled(ButtonV2)` - height: ${props => (props.height || "100%")}; - width: ${props => (props.width || "100%")}; + height: ${(props) => props.height || '100%'}; + width: ${(props) => props.width || '100%'}; `; const AdaptiveMobileItemHV2 = styled(ItemHV2)` @media (max-width: 767px) { justify-content: center; } -` +`; const AdaptiveMobileItemHV22 = styled(ItemHV2)` @media (max-width: 767px) { justify-content: center; flex-direction: column; } -` +`; const ImageSection = styled.img` width: 128px; @@ -239,7 +431,6 @@ const VerifyingName = styled.div``; const Subscribers = styled.div` width: 58px; height: 26px; - margin-bottom: 10px; background: #ffdbf0; color: #cf1c84; border-radius: 25px; @@ -269,7 +460,6 @@ const ChanneStateText = styled(StateText)` color: ${(props) => (props.active ? '#2DBD81' : '#E93636')}; background-color: ${(props) => (props.active ? '#c6efd1' : '#FFD8D8')}; margin-left: 10px; - margin-bottom: 10px; ${(props) => props.active && ` @@ -321,8 +511,8 @@ const Date = styled.div` align-items: flex-start; width: 340px; // color: #657795; - color: ${(props)=>props.theme.default.secondaryColor}; - margin: 10px 0; + color: ${(props) => props.theme.default.secondaryColor}; + margin-top: 10px; text-transform: none; font-weight: 500; font-size: 15px; @@ -351,11 +541,11 @@ const ChannelName = styled.div` font-family: Strawford, Source Sans Pro; flex-direction: row; margin-right: 8px; - margin-top: 40px; + margin-top: 12px; font-weight: 500; font-size: 30px; line-height: 141%; - text-align:center; + text-align: center; color: ${(props) => props.theme.color}; @media (max-width: 767px) { flex-direction: column; @@ -382,18 +572,17 @@ const SectionDes = styled.div` text-transform: none; font-family: Strawford, Source Sans Pro; // color: #657795; - color: ${(props)=>props.theme.default.secondaryColor}; - margin: ${(props) => (props.margin ? props.margin : '25px 0px 40px 0px')}; + color: ${(props) => props.theme.default.secondaryColor}; + margin: ${(props) => (props.margin ? props.margin : '24px 0px')}; font-weight: 400; font-size: 15px; line-height: 140%; - padding: 0px 20px 0px 10px; text-align: left; @media (max-width: 767px) { text-align: center; font-weight: 300; margin-top: 10px; - width:100%; + width: 100%; margin: 10px 0px 10px 0px; padding: 0 0 0 0; } @@ -401,9 +590,9 @@ const SectionDes = styled.div` const SubmitButton = styled(Button)` width: fit-content; - background: #D53A94; + background: #d53a94; color: #fff; - z-Index:0; + z-index: 0; font-family: 'Strawford'; font-style: normal; font-weight: 500; @@ -412,5 +601,15 @@ const SubmitButton = styled(Button)` margin-right: 9px; border-radius: 8px; padding: 10px 16px; - +`; + +const DelegateContainer = styled(Item)` + flex: 5; + min-width: 280px; + align-self: stretch; + align-items: stretch; + margin: 10px 0px 30px 0px; + border-radius: 20px; + border: 1px solid; + border-color: ${(props) => props.theme.default.borderColor}; `; \ No newline at end of file diff --git a/src/components/ChannelReactivateModalContent.tsx b/src/components/ChannelReactivateModalContent.tsx index 782dd79799..c44c8db9f8 100644 --- a/src/components/ChannelReactivateModalContent.tsx +++ b/src/components/ChannelReactivateModalContent.tsx @@ -530,7 +530,7 @@ const Footer = styled(ItemHV2)` align-content: space-between; justify-content: space-between; grid-gap: 40px; - transform: translateY(40px); + z-index: 1; @media (max-width: 600px) { padding: 16px; diff --git a/src/components/ChannelSettingsDropdown.tsx b/src/components/ChannelSettingsDropdown.tsx index f2f4c315eb..2cf19c86f6 100644 --- a/src/components/ChannelSettingsDropdown.tsx +++ b/src/components/ChannelSettingsDropdown.tsx @@ -13,19 +13,15 @@ import styled, { useTheme } from 'styled-components'; import { postReq } from 'api'; import LoaderSpinner, { LOADER_TYPE } from 'components/reusables/loaders/LoaderSpinner'; import EPNSCoreHelper from 'helpers/EPNSCoreHelper'; -import useModalBlur, {MODAL_POSITION} from 'hooks/useModalBlur'; +import useModalBlur, { MODAL_POSITION } from 'hooks/useModalBlur'; import useToast from 'hooks/useToast'; import { setUserChannelDetails } from 'redux/slices/adminSlice'; import cubeIcon from '../assets/icons/cube.png'; import redBellIcon from '../assets/icons/redBellSlash.png'; import greenBellIcon from '../assets/icons/greenBell.svg'; -import userMinusIcon from '../assets/icons/userCircleMinus.png'; -import userPlusIcon from '../assets/icons/userCirclePlus.png'; -import AddDelegateModalContent from './AddDelegateModalContent'; import AddSubgraphModalContent from './AddSubgraphModalContent'; import ChannelDeactivateModalContent from './ChannelDeactivateModalContent'; import ChannelReactivateModalContent from './ChannelReactivateModalContent'; -import RemoveDelegateModalContent from './RemoveDelegateModalContent'; // Internal Configs import { abis, addresses, appConfig } from 'config'; @@ -66,16 +62,6 @@ function ChannelSettings({ DropdownRef, isDropdownOpen, closeDropdown }: Channel showModal: showReactivateChannelModal, ModalComponent: ReactivateChannelModalComponent, } = useModalBlur(); - const { - isModalOpen: isAddDelegateModalOpen, - showModal: showAddDelegateModal, - ModalComponent: AddDelegateModalComponent, - } = useModalBlur(); - const { - isModalOpen: isRemoveDelegateModalOpen, - showModal: showRemoveDelegateModal, - ModalComponent: RemoveDelegateModalComponent, - } = useModalBlur(); const { isModalOpen: isAddSubgraphModalOpen, showModal: showAddSubgraphModal, @@ -87,8 +73,6 @@ function ChannelSettings({ DropdownRef, isDropdownOpen, closeDropdown }: Channel isDropdownOpen && !isDeactivateChannelModalOpen && !isReactivateChannelModalOpen && - !isAddDelegateModalOpen && - !isRemoveDelegateModalOpen && !isAddSubgraphModalOpen; useClickAway(DropdownRef, () => closeDropdownCondition && closeDropdown()); @@ -156,16 +140,6 @@ function ChannelSettings({ DropdownRef, isDropdownOpen, closeDropdown }: Channel */ const deactivateChannel = () => epnsWriteProvider.deactivateChannel(); - const addDelegateToast = useToast(); - const addDelegate = async (walletAddress: string) => { - return epnsCommWriteProvider.addDelegate(walletAddress); - }; - - const removeDelegateToast = useToast(); - const removeDelegate = (walletAddress: string) => { - return epnsCommWriteProvider.removeDelegate(walletAddress); - }; - const addSubgraphToast = useToast(); const addSubgraphDetails = async (pollTime, subGraphId) => { // design not present to show below cases @@ -220,34 +194,6 @@ function ChannelSettings({ DropdownRef, isDropdownOpen, closeDropdown }: Channel )} - !channelInactive && showAddDelegateModal()} - > -
- -
- Add Delegate -
- - - !channelInactive && showRemoveDelegateModal()} - > -
- -
- Remove Delegate -
- - {onCoreNetwork && ( - {/* modal to add a delegate */} - - - {/* modal to remove a delegate */} - - {/* modal to add a subgraph */} { +const DelegateInfo = ({ delegateAddress, maxWidth }) => { const [addressText, setAddressText] = useState(delegateAddress); const [isCopied, setIsCopied] = useState(false); const isMobile = useDeviceWidthCheck(1200); @@ -26,27 +25,14 @@ const DelegateInfo = ({ delegateAddress, isDelegate, maxWidth }) => { }, [isMobile]); return ( - <> - {!isDelegate ? ( - setIsCopied(false)} - minWidth={!isMobile ? "350px" : "120px"} - > - - - ) : ( - setIsCopied(false)} - minWidth={!isMobile ? "350px" : "120px"} - > - - - )} - + setIsCopied(false)} + minWidth={!isMobile ? "350px" : "120px"} + > + + ); }; @@ -57,6 +43,14 @@ const WalletInfoContent = ({ delegateAddress, }) => { const isMobile = useDeviceWidthCheck(1000); + const [isHovered, setIsHovered] = useState(false); + + const handleMouseOut = (e) => { + setIsHovered(false); + } + const handleMouseOver = (e) => { + setIsHovered(true); + } return (
-
{addressText}
- {shortenText(addressText, 7, 7)} + {isHovered && { navigator.clipboard.writeText(delegateAddress); @@ -80,7 +76,7 @@ const WalletInfoContent = ({ ) : ( )} - + }
); }; @@ -94,7 +90,7 @@ const WalletAddressDisplay = styled.span` flex: 3; // margin-right: 30px; // margin-left: 10px; - padding: 6px 25px; + padding: 0px 15px; max-height: 30px; display: flex; align-items: baseline; @@ -134,10 +130,11 @@ const HoverWallet = styled(WalletAddressDisplay)` } `; -const Wallet = styled(WalletAddressDisplay)` - color: #fff; - background: rgb(226, 8, 128); - background: linear-gradient(87.17deg, #B6A0F5 0%, #F46EF7 57.29%, #FF95D5 100%); +const Address = styled.div` + padding-top: 3px; + font-size: 15px; + font-weight: 400; + &:hover { opacity: 0.9; cursor: pointer; diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx index f90cd063d9..9aeda79d88 100644 --- a/src/components/Dropdown.tsx +++ b/src/components/Dropdown.tsx @@ -1,6 +1,7 @@ // React + Web3 Essentials import { shortenText } from 'helpers/UtilityHelper'; import React from 'react'; +import { Link } from 'react-router-dom'; // External Packages import styled, { useTheme } from 'styled-components'; @@ -15,6 +16,7 @@ export type DropdownValueType = { icon: string, textColor?: string, function: () => void, + to?: string; // Add the 'to' property for navigation } type DropdownProps = { @@ -118,7 +120,7 @@ function Dropdown({ dropdownValues, textColor, iconFilter, hoverBGColor }: Dropd cursor="pointer" /> )} - {!dropdownValue?.link && dropdownValue?.function && ( + {!dropdownValue?.to && !dropdownValue?.link && dropdownValue?.function && ( )} + {dropdownValue?.to && ( + + {/* You can customize the Link as needed */} + + {dropdownValue.title} + + + )} {dropdownValue?.link && ( { const [showInbox, setShowInbox] = useState(!isSpam); @@ -34,10 +35,10 @@ const InboxComponent = ({isSpam}) => { - handleToggle('/inbox')}> + handleToggle(APP_PATHS.Inbox)}> Inbox - handleToggle('/spam')}> + handleToggle(APP_PATHS.Spam)}> Spam diff --git a/src/components/InitState.tsx b/src/components/InitState.tsx index f78fb8dd2b..9890124709 100644 --- a/src/components/InitState.tsx +++ b/src/components/InitState.tsx @@ -27,6 +27,7 @@ import { setUserChannelDetails, } from 'redux/slices/adminSlice'; import { setProcessingState } from 'redux/slices/channelCreationSlice'; +import { updateBulkChannelSettings } from 'redux/slices/channelSlice'; import { setPushAdmin } from 'redux/slices/contractSlice'; import { getChannelsSearch, getUserDelegations } from 'services'; import * as PushAPI from '@pushprotocol/restapi'; @@ -175,6 +176,12 @@ const InitState = () => { } const channelInformation = await Promise.all(channelInformationPromise); dispatch(setDelegatees(channelInformation)); + // get channel settings of all the channels + const channelSettings = {}; + for (const channel of channelInformation) { + channelSettings[channel.channel] = channel.channel_settings ? JSON.parse(channel.channel_settings) : []; + } + dispatch(updateBulkChannelSettings(channelSettings)); } else { dispatch(setDelegatees([])); } diff --git a/src/components/RemoveDelegateModalContent.tsx b/src/components/RemoveDelegateModalContent.tsx deleted file mode 100644 index 0c3d061908..0000000000 --- a/src/components/RemoveDelegateModalContent.tsx +++ /dev/null @@ -1,112 +0,0 @@ -// React + Web3 Essentials -import React from 'react'; - -// External Packages -import styled, { useTheme } from 'styled-components'; -import { useClickAway } from 'react-use'; -import { MdCheckCircle, MdError } from 'react-icons/md'; - -// Internal Components -import ModalHeader from 'primaries/SharedModalComponents/ModalHeader'; -import ModalInput from 'primaries/SharedModalComponents/ModalInput'; -import ModalConfirmButton from 'primaries/SharedModalComponents/ModalConfirmButton'; -import { ModalInnerComponentType } from 'hooks/useModalBlur'; - -// Internal Configs -import { device } from 'config/Globals'; - -const RemoveDelegateModalContent = ({ - onConfirm: removeDelegate, - onClose, - toastObject, -}: ModalInnerComponentType) => { - const delegateAddressInputRef = React.useRef(); - - const [isLoading, setIsLoading] = React.useState(false); - - const theme = useTheme(); - - const handleClose = () => !isLoading && onClose(); - - // to close the modal upon a click on backdrop - const containerRef = React.useRef(null); - useClickAway(containerRef, () => handleClose()); - - const removeDelegateHandler = () => { - const delegateAddress = delegateAddressInputRef?.current?.value; - - setIsLoading(true); - - removeDelegate(delegateAddress) - .then(async (tx) => { - console.log(tx); - - toastObject.showMessageToast({ - toastTitle: 'Delegate Removed', - toastMessage: 'Delegate has been removed successfully', - toastType: 'SUCCESS', - getToastIcon: (size) => ( - - ), - }); - onClose(); - }) - .catch((err) => { - console.log({ err }); - - toastObject.showMessageToast({ - toastTitle: 'Transaction Failed', - toastMessage: 'Removing a delegate failed.', - toastType: 'ERROR', - getToastIcon: (size) => ( - - ), - }); - }) - .finally(() => { - setIsLoading(false); - }); - }; - - return ( - - - - - - ); -}; - -const ModalContainer = styled.div` - width: 30vw; - display: flex; - flex-direction: column; - margin: 6% 1%; - background: ${(props) => props.theme.modalContentBackground}; - border-radius: 1rem; - padding: 1.2% 2%; - @media (${device.laptop}) { - width: 50vw; - } - @media (${device.mobileL}) { - width: 95vw; - } -`; - -export default RemoveDelegateModalContent; diff --git a/src/components/SendNotifications.tsx b/src/components/SendNotifications.tsx index 9d09b504c2..e834849437 100644 --- a/src/components/SendNotifications.tsx +++ b/src/components/SendNotifications.tsx @@ -1,6 +1,5 @@ // React + Web3 Essentials -import { ethers } from 'ethers'; -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; // External Packages import Switch from '@material-ui/core/Switch'; @@ -13,12 +12,13 @@ import { MdCheckCircle, MdError } from 'react-icons/md'; import { useSelector } from 'react-redux'; import 'react-toastify/dist/ReactToastify.min.css'; import styled, { useTheme } from 'styled-components'; +import Slider from 'react-input-slider'; // Internal Compoonents import * as PushAPI from '@pushprotocol/restapi'; import { postReq } from 'api'; import LoaderSpinner, { LOADER_TYPE } from 'components/reusables/loaders/LoaderSpinner'; -import { SectionV2 } from 'components/reusables/SharedStylingV2'; +import { AInlineV2, SectionV2, SpanV2 } from 'components/reusables/SharedStylingV2'; import { convertAddressToAddrCaip } from 'helpers/CaipHelper'; import CryptoHelper from 'helpers/CryptoHelper'; import { IPFSupload } from 'helpers/IpfsHelper'; @@ -41,6 +41,8 @@ import PreviewNotif from './PreviewNotif'; // Internal Configs import { appConfig } from 'config'; import { useAccount, useDeviceWidthCheck } from 'hooks'; +import APP_PATHS from 'config/AppPaths'; +import Tag from './reusables/labels/Tag'; // Constants const CORE_CHAIN_ID = appConfig.coreContractChain; @@ -128,27 +130,79 @@ function SendNotifications() { }); const onCoreNetwork = CORE_CHAIN_ID === chainId; - const [nfProcessing, setNFProcessing] = React.useState(0); - const [channelAddress, setChannelAddress] = React.useState(''); - const [nfRecipient, setNFRecipient] = React.useState(account); - const [multipleRecipients, setMultipleRecipients] = React.useState([]); - const [tempRecipeint, setTempRecipient] = React.useState(''); // to temporarily hold the address of one recipient who would be entered into the recipeints array above. - const [nfType, setNFType] = React.useState('1'); - const [nfSub, setNFSub] = React.useState(''); - const [nfSubEnabled, setNFSubEnabled] = React.useState(false); - const [nfMsg, setNFMsg] = React.useState(''); - const [nfCTA, setNFCTA] = React.useState(''); - const [nfCTAEnabled, setNFCTAEnabled] = React.useState(false); - const [nfMedia, setNFMedia] = React.useState(''); - const [nfMediaEnabled, setNFMediaEnabled] = React.useState(false); - const [nfInfo, setNFInfo] = React.useState(''); - const [delegateeOptions, setDelegateeOptions] = React.useState([]); - const [errorMessage, setErrorMessage] = React.useState(''); - + const [nfProcessing, setNFProcessing] = useState(0); + const [channelAddress, setChannelAddress] = useState(''); + const [nfRecipient, setNFRecipient] = useState(account); + const [multipleRecipients, setMultipleRecipients] = useState([]); + const [tempRecipeint, setTempRecipient] = useState(''); // to temporarily hold the address of one recipient who would be entered into the recipeints array above. + const [nfType, setNFType] = useState('1'); + const [nfSub, setNFSub] = useState(''); + const [nfSubEnabled, setNFSubEnabled] = useState(false); + const [nfMsg, setNFMsg] = useState(''); + const [nfCTA, setNFCTA] = useState(''); + const [nfCTAEnabled, setNFCTAEnabled] = useState(false); + const [nfMedia, setNFMedia] = useState(''); + const [nfMediaEnabled, setNFMediaEnabled] = useState(false); + const [nfInfo, setNFInfo] = useState(''); + const [nfSettingType, setNFSettingType] = useState(null); + const [delegateeOptions, setDelegateeOptions] = useState([]); + const [nfSliderValue, setNfSliderValue] = useState(0); + + const channelDetailsFromBackend = useMemo(() => { + if (delegatees) { + return delegatees.find(delegatee => delegatee.channel === channelAddress); + } + // Return a default value or handle the case when delegatees is not defined. + return null; // or some other default value + }, [delegatees, channelAddress]); + + const channelSettings = useMemo(() => { + if (channelDetailsFromBackend) { + const { channel_settings } = channelDetailsFromBackend; + + if (channel_settings !== null) { + return JSON.parse(channel_settings); + } + } + // Return a default value or handle the case when channelDetailsFromBackend is not defined. + return null; // or some other default value + }, [channelDetailsFromBackend]); + + const channelSettingsOptions = useMemo(() => { + const defaultOption = { label: 'Default', value: null, isRange: false }; + + if (channelSettings) { + const settingsOptions = channelSettings.map((setting) => ({ + label: + setting.type === 2 ? ( + +
{setting.description}
+ Range +
+ ) : ( + setting.description + ), + value: setting.index, + isRange: setting.type === 2, + })); + + return [defaultOption, ...settingsOptions]; + } + // If channelSettings is not defined, just return the default option. + return [defaultOption]; + }, [channelSettings]); + + const openManageSettings = () => { + const newPageUrl = APP_PATHS.ChannelSettings; + + // Use window.open() to open the URL in a new tab + window.open(newPageUrl, '_blank'); + } + useEffect(() => { if (canSend !== 1) { const url = window.location.origin; - window.location.replace(`${url}/#/channels`); + window.location.replace(`${url}${APP_PATHS.Channels}`); } }); @@ -163,7 +217,7 @@ function SendNotifications() { (delegatees.length === 1 && delegatees[0].alias_address === account) || !delegatees.length; // construct a list of channel delegators - React.useEffect(() => { + useEffect(() => { if (!account) return; if (!delegatees || !delegatees.length) { setChannelAddress(account); @@ -190,31 +244,6 @@ function SendNotifications() { } }, [delegatees, account]); - // const isAllFieldsFilled = () => { - // if (nfRecipient == "" || - // nfType == "" || - // nfMsg == "" || - // (nfSubEnabled && nfSub == "") || - // (nfCTAEnabled && nfCTA == "") || - // (nfMediaEnabled && nfMedia == "") - // ) { - // return false; - // } - // return true; - // }; - - // const previewNotif = (e: any) => { - // e.preventDefault(); - // if(isAllFieldsFilled()) - // setPreviewNotifModalOpen(true) - // else { - // setNFInfo("Please fill all fields to preview"); - // setTimeout(() => { - // setNFInfo(''); - // }, 2000); - // } - // } - // on change for the subset type notifications input const handleSubsetInputChange = (e: any) => { // if the user enters in a comma or an enter then separate the addresses @@ -238,7 +267,7 @@ function SendNotifications() { setMultipleRecipients(filteredRecipients); }; - React.useEffect(() => { + useEffect(() => { const broadcastIds = ['1']; //id's of notifications which qualify as broadcast setMultipleRecipients([]); //reset array when type changes/ if (broadcastIds.includes(nfType)) { @@ -268,6 +297,14 @@ function SendNotifications() { return validated; }; + const getIndex = () => { + if (nfSettingType === null) return undefined; + else if (channelSettings[nfSettingType - 1]?.type === 1) + return `${nfSettingType}-1`; + else if (channelSettings[nfSettingType - 1]?.type === 2) + return `${nfSettingType}-2-${nfSliderValue}`; + } + const handleSendMessage = async (e) => { // Check everything in order e.preventDefault(); @@ -296,128 +333,6 @@ function SendNotifications() { let acta = nfCTA; let aimg = nfMedia; - // Decide type and storage - // switch (nfType) { - // // Broadcast Notification - // case "1": - // break; - - // // Targeted Notification - // case "3": - // break; - - // // Old Secret Notification - // // case "2": - // // // Create secret - // // let secret = CryptoHelper.makeid(14); - - // // // Encrypt payload and change sub and nfMsg in notification - // // nsub = "You have a secret message!"; - // // nmsg = "Open the app to see your secret message!"; - - // // // get public key from EPNSCoreHelper - // // let k = await EPNSCoreHelper.getPublicKey( - // // nfRecipient, - // // epnsCommWriteProvider - // // ); - // // if (k == null) { - // // // No public key, can't encrypt - // // setNFInfo( - // // "Public Key Registration is required for encryption!" - // // ); - // // setNFProcessing(2); - - // // toast.update(notificationToast, { - // // render: "Unable to encrypt for this user, no public key registered", - // // type: toast.TYPE.ERROR, - // // autoClose: 5000, - // // }); - - // // return; - // // } - - // // let publickey = k.toString().substring(2); - // // //console.log("This is public Key: " + publickey); - - // // secretEncrypted = await CryptoHelper.encryptWithECIES( - // // secret, - // // publickey - // // ); - // // asub = CryptoHelper.encryptWithAES(nfSub, secret); - // // amsg = CryptoHelper.encryptWithAES(nfMsg, secret); - // // acta = CryptoHelper.encryptWithAES(nfCTA, secret); - // // aimg = CryptoHelper.encryptWithAES(nfMedia, secret); - // // break; - - // // Targeted Notification - // case "4": - // break; - - // // Secret Notification - // case "5": - // // Create secret - // let secret = CryptoHelper.makeid(8); - - // // Encrypt payload and change sub and nfMsg in notification - // nsub = "You have a secret message!"; - // nmsg = "Click on Decrypt button to see your secret message!"; - - // // get public key from Backend API - // let encryptionKey = await postReq('/encryption_key/get_encryption_key', { - // address: nfRecipient, - // op: "read" - // }).then(res => { - // return res.data?.encryption_key; - // }); - - // if (encryptionKey == null) { - // // No public key, can't encrypt - // setNFInfo( - // "Public Key Registration is required for encryption!" - // ); - // setNFProcessing(2); - - // toast.update(notificationToast, { - // render: "Unable to encrypt for this user, no public key registered", - // type: toast.TYPE.ERROR, - // autoClose: 5000, - // }); - - // return; - // } - - // let publickey = encryptionKey; - - // secretEncrypted = await CryptoHelper.encryptWithRPCEncryptionPublicKey( - // secret, - // publickey - // ); - // // console.log(secretEncrypted); - // if(nfSubEnabled) asub = CryptoHelper.encryptWithAES(nfSub, secret); - // amsg = CryptoHelper.encryptWithAES(nfMsg, secret); - // if(nfCTAEnabled) acta = CryptoHelper.encryptWithAES(nfCTA, secret); - // if(nfMediaEnabled) aimg = CryptoHelper.encryptWithAES(nfMedia, secret); - // break; - - // // Offchain Notification - // case "6": - // console.log( - // nsub, - // nmsg, - // nfType, - // asub, - // amsg, - // acta, - // aimg, - // "case 5" - // ); - - // break; - - // default: - // break; - // } - // Handle Storage let storagePointer = ''; @@ -476,105 +391,8 @@ function SendNotifications() { }); return; } - - // const jsonPayload = { - // notification: { - // title: nsub, - // body: nmsg, - // }, - // data: { - // type: nfType, - // secret: secretEncrypted, - // asub: asub, - // amsg: amsg, - // acta: acta, - // aimg: aimg, - // }, - // }; - - // // if we are sending a subset type, then include recipients - // if (nfType === "4") { - // jsonPayload["recipients"] = [...multipleRecipients]; - // } - - // const input = JSON.stringify(jsonPayload); - // console.log(input); - - // console.log("Uploding to IPFS..."); - // toast.update(notificationToast, { - // render: "Preparing Payload for upload", - // }); - - // const ipfs = require("nano-ipfs-store").at( - // "https://ipfs.infura.io:5001" - // ); - - // try { - // // storagePointer = await ipfs.add(input); - // storagePointer = await IPFSupload(input); - // } catch (e) { - // setNFProcessing(2); - // setNFInfo("IPFS Upload Error"); - // } - - // console.log("IPFS cid: %o", storagePointer); } if (nfType === '1' || nfType === '2' || nfType === '3' || nfType === '4' || nfType === '5') { - // Prepare Identity and send notification - // const identity = nfType + "+" + storagePointer; - // const identityBytes = ethers.utils.toUtf8Bytes(identity); - // console.log({ - // identityBytes, - // }); - // const EPNS_DOMAIN = { - // name: "Push (EPNS) COMM V1", - // chainId: chainId, - // verifyingContract: epnsCommReadProvider.address, - // }; - - // const type = { - // Data: [ - // { name: "acta", type: "string" }, - // { name: "aimg", type: "string" }, - // { name: "amsg", type: "string" }, - // { name: "asub", type: "string" }, - // { name: "type", type: "string" }, - // { name: "secret", type: "string" }, - // ], - // }; - - // const payload = { - // data: { - // acta: acta, - // aimg: aimg, - // amsg: amsg, - // asub: asub, - // type: nfType, - // secret: "", - // }, - - // notification: { - // body: amsg, - // title: asub, - // }, - // }; - - // if (nfType === "5" || nfType === "2") { - // payload.notification = { - // body: nmsg, - // title: nsub - // }; - // payload.data.secret = secretEncrypted; - // } - - // const message = payload.data; - // console.log(payload, "payload"); - // console.log("chainId", chainId); - // const signature = await library - // .getSigner(account) - // ._signTypedData(EPNS_DOMAIN, type, message); - // console.log("case5 signature", signature); - try { // apiResponse?.status === 204, if sent successfully! @@ -601,6 +419,7 @@ function SendNotifications() { body: amsg, cta: acta, img: aimg, + index: getIndex(), }, recipients: notifRecipients, // recipient address channel: channelAddressInCaip, // your channel address @@ -659,161 +478,7 @@ function SendNotifications() { setNFProcessing(0); console.log(err); } - - // var anotherSendTxPromise; - - // anotherSendTxPromise = communicatorContract.sendNotification( - // channelAddress, - // nfRecipient, - // identityBytes - // ); - - // console.log("Sending Transaction... "); - // toast.update(notificationToast, { - // render: "Sending Notification...", - // }); - - // anotherSendTxPromise - // .then(async (tx) => { - // console.log(tx); - // console.log("Transaction Sent!"); - - // toast.update(notificationToast, { - // render: "Notification Sent", - // type: toast.TYPE.INFO, - // autoClose: 5000, - // }); - - // await tx.wait(1); - // console.log("Transaction Mined!"); - - // setNFProcessing(2); - // setNFType(""); - // setNFInfo("Notification Sent"); - - // toast.update(notificationToast, { - // render: "Transaction Mined / Notification Sent", - // type: toast.TYPE.SUCCESS, - // autoClose: 5000, - // }); - // }) - // .catch((err) => { - // console.log("!!!Error handleSendMessage() --> %o", err); - // setNFInfo("Transaction Failed, please try again"); - - // toast.update(notificationToast, { - // render: "Transacion Failed: " + err, - // type: toast.TYPE.ERROR, - // autoClose: 5000, - // }); - // setNFProcessing(0); - // }); } - // if (nfType === "6") { - // // const jsonPayload = { - // // notification: { - // // title: nsub, - // // body: nmsg, - // // }, - // // data: { - // // type: nfType, - // // secret: secretEncrypted, - // // asub: asub, - // // amsg: amsg, - // // acta: acta, - // // aimg: aimg, - // // }, - // // }; - - // const EPNS_DOMAIN = { - // name: "Push (EPNS) COMM V1", - // chainId: chainId, - // verifyingContract: epnsCommReadProvider.address, - // }; - - // const type = { - // Data: [ - // { name: "acta", type: "string" }, - // { name: "aimg", type: "string" }, - // { name: "amsg", type: "string" }, - // { name: "asub", type: "string" }, - // { name: "type", type: "string" }, - // { name: "secret", type: "string" }, - // ], - // }; - - // const payload = { - // data: { - // acta: acta, - // aimg: aimg, - // amsg: amsg, - // asub: asub, - // type: nfType, - // secret: "", - // }, - - // notification: { - // body: amsg, - // title: asub, - // }, - // }; - - // const message = payload.data; - // console.log(payload, "payload"); - // console.log("chainId", chainId); - // const signature = await library - // .getSigner(account) - // ._signTypedData(EPNS_DOMAIN, type, message); - // console.log("case5 signature", signature); - // try { - // postReq("/payloads/add_manual_payload", { - // signature, - // op: "write", - // chainId: chainId.toString(), - // channel: channelAddress, - // recipient: nfRecipient, - // deployedContract: epnsCommReadProvider.address, - // payload: payload, - // type: "3", - // }).then(async (res) => { - // toast.update(notificationToast, { - // render: "Notification Sent", - // type: toast.TYPE.INFO, - // autoClose: 5000, - // }); - - // setNFProcessing(2); - // setNFType(""); - // setNFInfo("Notification Sent"); - - // toast.update(notificationToast, { - // render: "Notification Sent", - // type: toast.TYPE.SUCCESS, - // autoClose: 5000, - // }); - // console.log(res); - // }); - // } catch (err) { - // if (err.code === 4001) { - // // EIP-1193 userRejectedRequest error - // toast.update(notificationToast, { - // render: "User denied message signature.", - // type: toast.TYPE.ERROR, - // autoClose: 5000, - // }); - // } else { - // setNFInfo("Sending Notification Failed, please try again"); - - // toast.update(notificationToast, { - // render: "Notification Failed: " + err, - // type: toast.TYPE.ERROR, - // autoClose: 5000, - // }); - // } - // setNFProcessing(0); - // console.log(err); - // } - // } }; const isEmpty = (field: any) => { @@ -920,32 +585,8 @@ function SendNotifications() { }} placeholder="Select a Channel" value={delegateeOptions[0]} - // value={delegateeOptions.find( - // (d) => - // d.value == - // channelAddress - // )} /> - {/* - - SEND NOTIFICATION ON BEHALF - OF - - { - setChannelAddress( - option.value - ); - }} - value={delegateeOptions.find( - (d) => - d.value == - channelAddress - )} - /> - */} )} @@ -990,10 +631,10 @@ function SendNotifications() { weight={isMobile ? "500" : "600"} textTransform="none" size={isMobile ? "15px" : "14px"} - color="#1E1E1E" + color={theme.default.color} padding="5px 15px" radius="30px"> - Subject + Title
setNFSubEnabled(!nfSubEnabled)} /> @@ -1003,10 +644,10 @@ function SendNotifications() { weight={isMobile ? "500" : "600"} textTransform="none" size={isMobile ? "15px" : "14px"} - color="#1E1E1E" + color={theme.default.color} padding="5px 15px" radius="30px"> - Media + Media URL setNFMediaEnabled(!nfMediaEnabled)} /> @@ -1016,7 +657,7 @@ function SendNotifications() { weight={isMobile ? "500" : "600"} textTransform="none" size={isMobile ? "15px" : "14px"} - color="#1E1E1E" + color={theme.default.color} padding="5px 15px" radius="30px"> CTA Link @@ -1025,7 +666,7 @@ function SendNotifications() { )} - + {(nfType === '2' || nfType === '3' || nfType === '5') && ( @@ -1036,10 +677,11 @@ function SendNotifications() { padding="12px" weight="400" size="16px" - bg="white" + color={theme.default.color} + bg={theme.default.bg} height="25px" margin="7px 0px 0px 0px" - border="1px solid #BAC4D6" + border={`1px solid ${theme.snfBorder}`} focusBorder="1px solid #657795" radius="12px" value={nfRecipient} @@ -1071,10 +713,11 @@ function SendNotifications() { padding="12px" weight="400" size="16px" - bg="white" + color={theme.default.color} + bg={theme.default.bg} height="25px" margin="7px 0px 0px 0px" - border="1px solid #BAC4D6" + border={`1px solid ${theme.snfBorder}`} focusBorder="1px solid #657795" radius="12px" value={tempRecipeint} @@ -1107,7 +750,7 @@ function SendNotifications() { justify="space-between" > { @@ -1182,6 +827,97 @@ function SendNotifications() { )} + {nfType && ( + <> + + + + + Manage Settings + + + + { + setNFSettingType(option.value); + if(channelSettings[option.value - 1]?.type === 2) { + setNfSliderValue(channelSettings[option.value - 1]?.default); + } + }} + value={channelSettingsOptions[0]} + /> + + + {nfSettingType !== null && channelSettings[nfSettingType - 1]?.type === 2 && ( + + + + setNfSliderValue(x)} + xstep={1} + xmin={channelSettings[nfSettingType - 1]?.lowerLimit} + xmax={channelSettings[nfSettingType - 1]?.upperLimit} + /> + {nfSliderValue} + + + )} + + )} + {nfType && nfMediaEnabled && ( @@ -1191,10 +927,11 @@ function SendNotifications() { padding="12px" weight="400" size="16px" - bg="white" + color={theme.default.color} + bg={theme.default.bg} height="25px" margin="7px 0px 0px 0px" - border="1px solid #BAC4D6" + border={`1px solid ${theme.snfBorder}`} focusBorder="1px solid #657795" radius="12px" value={nfMedia} @@ -1215,10 +952,11 @@ function SendNotifications() { padding="12px" weight="400" size="16px" - bg="white" + color={theme.default.color} + bg={theme.default.bg} height="25px" margin="7px 0px 0px 0px" - border="1px solid #BAC4D6" + border={`1px solid ${theme.snfBorder}`} radius="12px" focusBorder="1px solid #657795" value={nfCTA} @@ -1234,6 +972,7 @@ function SendNotifications() {
{nfInfo}
)} + {showPreview && ( props.theme.default.bg}; + color: ${(props) => props.theme.default.color}; + border: 1px solid ${(props) => props.theme.snfBorder}; border-radius: 12px; flex: 1; outline: none; @@ -1428,14 +1167,14 @@ const DropdownStyled = styled(Dropdown)` } .Dropdown-option { - background-color: #fff; - color: #000; + background-color: ${(props) => props.theme.default.bg}; + color: ${(props) => props.theme.default.color}; font-size: 16px; padding: 20px 20px; } .Dropdown-option:hover { background-color: #d00775; - color: #000; + color: white; } `; @@ -1482,7 +1221,7 @@ const CustomDropdownItem = styled.div` margin-right: 10px; } div { - color: black; + color: ${(props) => props.theme.default.color}; font-size: 16px; letter-spacing: 2px; } @@ -1515,7 +1254,7 @@ const ToggleOption = styled(ItemH)` box-sizing: border-box; margin: 15px 0px; width: 10em; - background: #f4f5fa; + background: ${(props) => props.theme.snfToggleBg}; flex: none; padding: 15px; border-radius: 20px; @@ -1544,5 +1283,11 @@ const SubmitButton = styled(Button)` } `; +const DropdownLabel = styled.div` + display: flex; + flex-direction: row; + align-items: center; +`; + // Export Default export default SendNotifications; diff --git a/src/components/SharedStyling.js b/src/components/SharedStyling.js index a01622ec83..81577a3695 100644 --- a/src/components/SharedStyling.js +++ b/src/components/SharedStyling.js @@ -57,12 +57,10 @@ export const Item = styled.div` flex: ${props => props.flex || '1'}; flex-basis: ${props => props.flexBasis || 'auto'}; flex-direction: ${props => props.direction || 'column'}; - flex-direction: column; flex-wrap: ${props => props.wrap || 'wrap'}; font-size: ${props => props.size || 'inherit'}; height: ${props => props.height || 'auto'}; justify-content: ${props => props.justify || 'center'}; - justify-content: center; left: ${props => props.left || 'auto'}; margin: ${props => props.margin || '0px'}; diff --git a/src/components/ShowDelegates.jsx b/src/components/ShowDelegates.jsx deleted file mode 100644 index e00fb44480..0000000000 --- a/src/components/ShowDelegates.jsx +++ /dev/null @@ -1,280 +0,0 @@ -// React + Web3 Essentials -import React, { useEffect, useState } from 'react'; - -// External Packages -import { AiOutlineUserDelete } from 'react-icons/ai'; -import { GoTriangleDown, GoTriangleUp } from 'react-icons/go'; -import { useSelector } from 'react-redux'; -import styled, { css, useTheme } from 'styled-components'; - -// Internal Compoonents -import { getReq } from 'api'; -import { ButtonV2 } from 'components/reusables/SharedStylingV2'; -import { convertAddressToAddrCaip } from 'helpers/CaipHelper'; -import { useAccount, useDeviceWidthCheck } from 'hooks'; -import useModalBlur, {MODAL_POSITION} from 'hooks/useModalBlur'; -import useToast from 'hooks/useToast'; -import { Button, Content, H2, H3, Item, Section, Span } from 'primaries/SharedStyling'; -import { getChannelDelegates } from 'services'; -import DelegateInfo from './DelegateInfo'; -import RemoveDelegateModalContent from './RemoveDelegateModalContent'; - -const isOwner = (account, delegate) => { - return account?.toLowerCase() !== delegate?.toLowerCase(); -}; - -const ShowDelegates = () => { - const { account, chainId } = useAccount(); - const [delegatees, setDelegatees] = React.useState([account]); - const theme = useTheme(); - const [isActiveDelegateDropdown, setIsActiveDelegateDropdown] = React.useState(true); - const [removeModalOpen, setRemoveModalOpen] = React.useState(false); - const [delegateToBeRemoved, setDelegateToBeRemoved] = React.useState(''); - const { epnsCommWriteProvider } = useSelector((state) => state.contracts); - const isMobile = useDeviceWidthCheck(700); - - const { - isModalOpen: isRemoveDelegateModalOpen, - showModal: showRemoveDelegateModal, - ModalComponent: RemoveDelegateModalComponent, - } = useModalBlur(); - - const removeDelegateToast = useToast(); - const removeDelegate = (walletAddress) => { - return epnsCommWriteProvider.removeDelegate(walletAddress); - }; - - useEffect(() => { - if(account) fetchDelegatees(); - }, [account]); - - const fetchDelegatees = async () => { - try { - const channelAddressinCAIP = convertAddressToAddrCaip(account, chainId); - const channelDelegates = await getChannelDelegates({ channelCaipAddress: channelAddressinCAIP }); - if (channelDelegates) { - const delegateeList = channelDelegates.map((delegate) => delegate); - delegateeList.unshift(account); - setDelegatees(delegateeList); - } - } catch (err) { - console.error(err); - } - }; - - const removeDelegateModalOpen = (delegateAddress) => { - setDelegateToBeRemoved(delegateAddress); - setRemoveModalOpen(true); - }; - - return ( - <> -
- - - Channel Delegates -
- Delegates that can send notifications on behalf of this channel. - - -
- - - {isActiveDelegateDropdown && delegatees && ( - - {delegatees.map((delegate, idx) => { - return ( - - - {isOwner(account, delegate) ? ( - - ) : ( - Channel Creator - )} - - ); - })} - - )} - - - - ); -}; - -const RemoveButton = ({ delegateAddress, removeDelegateModalOpen, showRemoveDelegateModal }) => { - const theme = useTheme(); - const [isHovered, setIsHovered] = useState(false); - - const handleMouseOver = () => { - setIsHovered(true); - }; - - const handleMouseOut = () => { - setIsHovered(false); - }; - - return ( - showRemoveDelegateModal()} - > - {isHovered ? ( -
- -
-
Remove Delegate
-
- ) : ( - Delegate - )} - - ); -}; - -const TextStyle = styled.div` - color: ${(props) => props.theme.default.secondaryColor}; - text-align: right; - width: 100%; -`; - -const ChannelActionButton = styled.button` - border: 0; - outline: 0; - display: flex; - align-items: center; - justify-content: center; - padding: 8px 15px; - color: #fff; - border-radius: 5px; - font-size: 14px; - font-weight: 400; - position: relative; - &:hover { - opacity: 0.9; - cursor: pointer; - pointer: hand; - } - &:active { - opacity: 0.75; - cursor: pointer; - pointer: hand; - } - ${(props) => - props.disabled && - css` - &:hover { - opacity: 1; - cursor: default; - pointer: default; - } - &:active { - opacity: 1; - cursor: default; - pointer: default; - } - `} -`; - -const DelegateContainer = styled(Item)` - flex: 5; - min-width: 280px; - align-self: stretch; - align-items: stretch; - margin: 10px 0px 30px 0px; - border-radius: 20px; - border: 1px solid; - border-color: ${(props) => props.theme.default.borderColor}; -`; - -const RemoveButtonUI = styled(ChannelActionButton)` - background: transparent; - color: ${(props) => props.theme.color}; - height: 36px; - max-width: 164px; - flex: 1; - font-style: normal; - font-weight: 600; - font-size: 14px; - line-height: 141%; - display: flex; - align-items: center; - text-align: right; - padding: 6px 10px 6px 9px; - gap: 5px; - - &:hover { - opacity: 0.9; - background: #e93636; - border-radius: 8px; - color: #fff; - } - cursor: pointer; -`; - -const OwnerButton = styled(Button)` - all: unset; - background: transparent; - font-weight: 500; - font-size: 16px; - color: #cf1c84; - cursor: auto; - - @media (max-width: 425px) { - font-weight: 400; - font-size: 14px; - } -`; - -const DelegatesInfoHeader = styled.div` - font-weight: 600; - font-size: 18px; - line-height: 150%; - display: flex; - align-items: center; - color: ${(props) => props.theme.color}; -`; - -const DelegatesInfoLabel = styled.div` - font-weight: 400; - font-size: 15px; - line-height: 140%; - // color: #657795; - color: ${(props) => props.theme.default.secondaryColor}; -`; - -export default ShowDelegates; diff --git a/src/components/StakingInfo.tsx b/src/components/StakingInfo.tsx index 40f7666d3e..ec6405f900 100644 --- a/src/components/StakingInfo.tsx +++ b/src/components/StakingInfo.tsx @@ -56,7 +56,7 @@ const StakingInfo = ({channelStakeFees, setStakeFeesChoosen, setProcessingInfo, {/* */} - +

Amount for Staking @@ -115,7 +115,7 @@ const TabSpace = styled.div` border-radius: 20px; background-color: #f4f5fa; align-items: center; - transform: translateY(40px); + z-index: 1; @media ${device.tablet} { width: 100%; diff --git a/src/components/ViewChannelItem.js b/src/components/ViewChannelItem.js index e58d85bc37..f33ba75faa 100644 --- a/src/components/ViewChannelItem.js +++ b/src/components/ViewChannelItem.js @@ -1,5 +1,5 @@ // React + Web3 Essentials -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; // External Packages import Skeleton from '@yisheng90/react-loading'; @@ -11,6 +11,7 @@ import { toast as toaster } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.min.css'; import styled, { css, useTheme } from 'styled-components'; import axios from 'axios'; +import { cloneDeep } from 'lodash'; // Internal Compoonents import * as PushAPI from '@pushprotocol/restapi'; @@ -19,7 +20,7 @@ import MetaInfoDisplayer from 'components/MetaInfoDisplayer'; import LoaderSpinner, { LOADER_TYPE } from 'components/reusables/loaders/LoaderSpinner'; import { convertAddressToAddrCaip } from 'helpers/CaipHelper'; import useToast from 'hooks/useToast'; -import { cacheChannelInfo, updateSubscriptionStatus } from 'redux/slices/channelSlice'; +import { cacheChannelInfo } from 'redux/slices/channelSlice'; import { addNewWelcomeNotif, incrementStepIndex } from 'redux/slices/userJourneySlice'; import ChannelTutorial, { isChannelTutorialized } from 'segments/ChannelTutorial'; import NotificationToast from '../primaries/NotificationToast'; @@ -35,6 +36,9 @@ import InfoImage from '../assets/info.svg'; import VerifiedTooltipContent from "./VerifiedTooltipContent"; import { IPFSGateway } from 'helpers/IpfsHelper'; import { useAccount, useDeviceWidthCheck } from 'hooks'; +import ManageNotifSettingDropdown from './dropdowns/ManageNotifSettingDropdown'; +import OptinNotifSettingDropdown from './dropdowns/OptinNotifSettingDropdown'; +import { ImageV2 } from './reusables/SharedStylingV2'; // Create Header function ViewChannelItem({ channelObjectProp, loadTeaser, playTeaser }) { @@ -48,7 +52,7 @@ function ViewChannelItem({ channelObjectProp, loadTeaser, playTeaser }) { (state) => state.contracts ); const { canVerify } = useSelector((state) => state.admin); - const { channelsCache, CHANNEL_BLACKLIST, subscriptionStatus } = useSelector((state) => state.channels); + const { channelsCache, CHANNEL_BLACKLIST, subscriptionStatus, userSettings: currentUserSettings } = useSelector((state) => state.channels); const { account, provider, chainId } = useAccount(); const onCoreNetwork = chainId === appConfig.coreContractChain; @@ -183,11 +187,9 @@ function ViewChannelItem({ channelObjectProp, loadTeaser, playTeaser }) { const generalToast = useToast(); - // to subscribe - const subscribe = async () => { - console.log('click executed'); - subscribeAction(false); - }; + const userSettings = useMemo(() => { + return cloneDeep(currentUserSettings); + }, [currentUserSettings]); const formatAddress = (addressText) => { return addressText.length > 40 ? `${shortenText(addressText, 4, 6)}` : addressText; @@ -322,125 +324,6 @@ function ViewChannelItem({ channelObjectProp, loadTeaser, playTeaser }) { }); }; - const subscribeToast = useToast(); - const subscribeAction = async () => { - setTxInProgress(true); - try { - let channelAddress = channelObject.channel; - if (!onCoreNetwork) { - channelAddress = channelObject.alias_address; - } - - subscribeToast.showLoaderToast({ loaderMessage: 'Waiting for Confirmation...' }); - - if (run) { - const type = { - Subscribe: [ - { name: 'channel', type: 'address' }, - { name: 'subscriber', type: 'address' }, - { name: 'action', type: 'string' }, - ], - }; - - const message = { - channel: channelAddress, - subscriber: account, - action: 'Subscribe', - }; - - await provider.getSigner(account)._signTypedData(EPNS_DOMAIN, type, message); - - console.log('in run'); - subscribeToast.showMessageToast({ - toastTitle: 'Success', - toastMessage: 'Successfully opted into channel !', - toastType: 'SUCCESS', - getToastIcon: (size) => ( - - ), - }); - - dispatch( - addNewWelcomeNotif({ - cta: '', - title: channelObject.info, - message: `Welcome to ${channelObject.name} Channel. From now onwards, you'll be getting notifications from this channel`, - icon: channelIcon, - url: channelObject.url, - sid: '', - app: channelObject.name, - image: '', - }) - ); - setTxInProgress(false); - setSubscribed(true); - if (stepIndex === 5) { - console.log('this is working'); - dispatch(incrementStepIndex()); - } - return; - } - - const _signer = await provider.getSigner(account); - await PushAPI.channels.subscribe({ - signer: _signer, - channelAddress: convertAddressToAddrCaip(channelAddress, chainId), // channel address in CAIP - userAddress: convertAddressToAddrCaip(account, chainId), // user address in CAIP - onSuccess: () => { - dispatch(updateSubscriptionStatus({ channelAddress: channelObject.channel, status: true })); - setSubscribed(true); - setSubscriberCount(subscriberCount + 1); - - subscribeToast.showMessageToast({ - toastTitle: 'Success', - toastMessage: 'Successfully opted into channel !', - toastType: 'SUCCESS', - getToastIcon: (size) => ( - - ), - }); - }, - onError: () => { - console.error('opt in error'); - subscribeToast.showMessageToast({ - toastTitle: 'Error', - toastMessage: `There was an error opting into channel`, - toastType: 'ERROR', - getToastIcon: (size) => ( - - ), - }); - }, - env: appConfig.pushNodesEnv, - }); - } catch (err) { - subscribeToast.showMessageToast({ - toastTitle: 'Error', - toastMessage: `There was an error opting into channel ( ${err.message} )`, - toastType: 'ERROR', - getToastIcon: (size) => ( - - ), - }); - - console.log(err); - } finally { - setTxInProgress(false); - } - }; - const copyToClipboard = (address) => { let hostname = window.location.hostname; // if we are on localhost, attach the port @@ -461,73 +344,6 @@ function ViewChannelItem({ channelObjectProp, loadTeaser, playTeaser }) { } }; - const unsubscribeToast = useToast(); - const unsubscribeAction = async () => { - try { - let channelAddress = channelObject.channel; - if (!onCoreNetwork) { - channelAddress = channelObject.alias_address; - } - - unsubscribeToast.showLoaderToast({ loaderMessage: 'Waiting for Confirmation...' }); - - const _signer = await provider.getSigner(account); - await PushAPI.channels.unsubscribe({ - signer: _signer, - channelAddress: convertAddressToAddrCaip(channelAddress, chainId), // channel address in CAIP - userAddress: convertAddressToAddrCaip(account, chainId), // user address in CAIP - onSuccess: () => { - dispatch(updateSubscriptionStatus({ channelAddress: channelObject.channel, status: false })); - setSubscribed(false); - setSubscriberCount(subscriberCount - 1); - - unsubscribeToast.showMessageToast({ - toastTitle: 'Success', - toastMessage: 'Successfully opted out of channel !', - toastType: 'SUCCESS', - getToastIcon: (size) => ( - - ), - }); - }, - onError: () => { - console.error('opt out error'); - unsubscribeToast.showMessageToast({ - toastTitle: 'Error', - toastMessage: `There was an error opting out of channel`, - toastType: 'ERROR', - getToastIcon: (size) => ( - - ), - }); - }, - env: appConfig.pushNodesEnv, - }); - } catch (err) { - unsubscribeToast.showMessageToast({ - toastTitle: 'Error', - toastMessage: `There was an error opting out of channel ( ${err.message} )`, - toastType: 'ERROR', - getToastIcon: (size) => ( - - ), - }); - - console.log(err); - } finally { - setTxInProgress(false); - } - }; - const correctChannelTitleLink = () => { const channelLink = CTA_OVERRIDE_CACHE[channelObject.channel] || channelObject.url; if (/(?:http|https):\/\//i.test(channelLink)) { @@ -1038,22 +854,31 @@ function ViewChannelItem({ channelObjectProp, loadTeaser, playTeaser }) { <> {isOwner && Owner} {!isOwner && ( - { + setSubscribed(true); + setSubscriberCount((prevSubscriberCount) => prevSubscriberCount + 1) + }} > - {txInProgress && ( - - - - )} - Opt-In - + {}} + disabled={txInProgress} + className="optin" + > + {txInProgress && ( + + + + )} + Opt-In + + )} )} @@ -1061,21 +886,39 @@ function ViewChannelItem({ channelObjectProp, loadTeaser, playTeaser }) { <> {isOwner && Owner} {!isOwner && ( - { + setSubscribed(false); + setSubscriberCount((prevSubscriberCount) => prevSubscriberCount - 1) + }} > - {txInProgress && ( - - - - )} - Opt-out - + {}} + disabled={txInProgress} + > + {txInProgress && ( + + + + )} + Manage + + + )} )} @@ -1433,8 +1276,6 @@ const ChannelActionButton = styled.button` display: flex; align-items: center; justify-content: center; - padding: 8px 15px; - margin: 10px; color: #fff; border-radius: 5px; font-size: 14px; @@ -1503,11 +1344,9 @@ const SkeletonButton = styled.div` const SubscribeButton = styled(ChannelActionButton)` background: #e20880; border-radius: 8px; - padding: 9px 15px; - min-width: 80px; - @media (max-width: 768px) { - padding: 9px 30px; - } + padding: 0px; + min-height: 36px; + min-width: 108px; `; const UnsubscribeButton = styled(ChannelActionButton)` @@ -1515,15 +1354,17 @@ const UnsubscribeButton = styled(ChannelActionButton)` color: ${(props) => props.theme.viewChannelPrimaryText}; border: 1px solid #bac4d6; border-radius: 8px; - padding: 9px 15px; - min-width: 80px; - @media (max-width: 768px) { - padding: 9px 30px; - } + padding: 0px 8px 0px 16px; + gap: 8px; + min-height: 36px; + min-width: 108px; `; const OwnerButton = styled(ChannelActionButton)` background: #35c5f3; + border-radius: 8px; + min-height: 36px; + min-width: 108px; `; const Toaster = styled.div` diff --git a/src/components/channel/AddSettingModalContent.tsx b/src/components/channel/AddSettingModalContent.tsx new file mode 100644 index 0000000000..987e69db2a --- /dev/null +++ b/src/components/channel/AddSettingModalContent.tsx @@ -0,0 +1,375 @@ +// React + Web3 Essentials +import React, { useState } from 'react'; + +// External Packages +import styled, { useTheme } from 'styled-components'; +import { useClickAway } from 'react-use'; +import { MdClose } from 'react-icons/md'; + +// Internal Components +import ModalConfirmButton from 'primaries/SharedModalComponents/ModalConfirmButton'; +import { ModalInnerComponentType } from 'hooks/useModalBlur'; +import type { ChannelSetting } from '../../helpers/channel/types'; + +// Internal Configs +import { device } from 'config/Globals'; +import { Item } from 'components/SharedStyling'; +import { FormSubmision, Input, Span } from 'primaries/SharedStyling'; +import { IOSSwitch } from 'components/SendNotifications'; +import { isAllFilledAndValid } from 'helpers/channel/InputValidation'; + +const ToggleItem = ({ checked, onChange, label, description }) => { + return ( + + + + + {description} + + + + + ); +}; + +interface AddSettingModalProps extends Omit { + InnerComponentProps?: { + settingToEdit?: ChannelSetting; + }; +} + +const AddSettingModalContent = ({ + onConfirm: onSubmit, + onClose, + toastObject, + InnerComponentProps, +}: AddSettingModalProps) => { + const settingToEdit = InnerComponentProps?.settingToEdit || undefined; + const [isLoading, setIsLoading] = useState(false); + const [settingName, setSettingName] = useState(settingToEdit ? settingToEdit.description : ''); + const [isDefault, setIsDefault] = useState( + settingToEdit + ? (settingToEdit.type === 1 && settingToEdit.default) || (settingToEdit.type === 2 && settingToEdit.enabled) + : true + ); + const [isRange, setIsRange] = useState(settingToEdit && settingToEdit.type === 2 ? true : false); + const [lowerLimit, setLowerLimit] = useState( + settingToEdit && settingToEdit.type === 2 ? settingToEdit.lowerLimit.toString() : '' + ); + const [upperLimit, setUpperLimit] = useState( + settingToEdit && settingToEdit.type === 2 ? settingToEdit.upperLimit.toString() : '' + ); + const [defaultValue, setDefaultValue] = useState( + settingToEdit && settingToEdit.type === 2 ? settingToEdit.default.toString() : '' + ); + const [errorInfo, setErrorInfo] = useState(); + + const theme = useTheme(); + + const handleClose = () => !isLoading && onClose(); + + const containerRef = React.useRef(null); + useClickAway(containerRef, () => handleClose()); + + const onConfirm = (event) => { + event.preventDefault(); + setIsLoading(true); + if ( + isAllFilledAndValid({ + setErrorInfo, + defaultValue, + settingName, + lowerLimit, + type: isRange ? 2 : 1, + upperLimit, + }) + ) { + const index = settingToEdit ? settingToEdit.index : Math.floor(Math.random() * 1000000); + const settingData: ChannelSetting = isRange + ? { + type: 2, + default: Number(defaultValue), + enabled: isDefault, + description: settingName, + index: index, + lowerLimit: Number(lowerLimit), + upperLimit: Number(upperLimit), + } + : { + type: 1, + default: isDefault, + description: settingName, + index: index, + }; + onSubmit(settingData); + onClose(); + } + setIsLoading(false); + }; + + return ( + + + + {settingToEdit ? 'Edit ' : 'Add a '} Setting + + + + + {50 - settingName.length} + + + { + setSettingName(e.target.value.slice(0, 50)); + setErrorInfo((prev) => ({ ...prev, settingName: undefined })); + }} + autocomplete="off" + hasError={errorInfo?.settingName ? true : false} + /> + {errorInfo?.settingName} + + setIsDefault((prev) => !prev)} + label="Set as default" + description="Setting turned on for users by default" + /> + setIsRange((prev) => !prev)} + label="Range" + description="Set a range for this setting e.g. 1-10" + /> + {isRange && ( + <> + + + + { + setLowerLimit(e.target.value); + setErrorInfo((prev) => ({ ...prev, lowerLimit: undefined })); + }} + autocomplete="off" + hasError={errorInfo?.lowerLimit ? true : false} + /> + + { + setUpperLimit(e.target.value); + setErrorInfo((prev) => ({ ...prev, upperLimit: undefined })); + }} + autocomplete="off" + hasError={errorInfo?.upperLimit ? true : false} + /> + + {errorInfo?.lowerLimit} + {errorInfo?.upperLimit} + + + + { + setDefaultValue(e.target.value); + setErrorInfo((prev) => ({ ...prev, default: undefined })); + }} + autocomplete="off" + hasError={errorInfo?.default ? true : false} + /> + {errorInfo?.default} + + + )} + + + + ); +}; + +const CloseButton = styled(MdClose)` + align-self: flex-end; + color: ${(props) => props.theme.default.secondaryColor}; + font-size: 20px; + cursor: pointer; +`; + +const ModalTitle = styled.div` + font-size: 24px; + font-weight: 500; + line-height: 29px; + letter-spacing: -0.02em; + text-align: center; + color: ${(props) => props.theme.default.color}; +`; + +const ModalContainer = styled.div` + width: 30vw; + display: flex; + flex-direction: column; + margin: 6% 1%; + background: ${(props) => props.theme.modalContentBackground}; + border-radius: 1rem; + padding: 1.2% 2%; + @media (${device.laptop}) { + width: 50vw; + } + @media (${device.mobileL}) { + width: 95vw; + } +`; + +const Label = styled.div<{ padding?: string }>` + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 150%; + letter-spacing: -0.011em; + color: ${(props) => props.theme.default.color}; + padding: ${(props) => props.padding || '0px'}; +`; + +const Description = styled.div` + font-size: 12px; + font-weight: 400; + line-height: 16px; + letter-spacing: 0em; + text-align: left; + color: ${(props) => props.theme.default.secondaryColor}; +`; + +const MaxWidthInput = styled(Input)<{ hasError: boolean }>` + max-width: 108px; + flex: 1; + border: ${(props) => + props.hasError ? `1px solid ${props.theme.nfsError}` : `1px solid ${props.theme.default.borderColor}`}; +`; + +const InputWithError = styled(Input)<{ hasError: boolean }>` + flex: 1; + border: ${(props) => + props.hasError ? `1px solid ${props.theme.nfsError}` : `1px solid ${props.theme.default.borderColor}`}; +`; + +const ErrorInfo = styled.span` + font-size: 12px; + font-weight: 500; + line-height: 18px; + letter-spacing: 0em; + text-align: left; + color: ${(props) => props.theme.nfsError}; + margin-top: 4px; +`; + +export default AddSettingModalContent; diff --git a/src/components/channel/ChannelButtons.tsx b/src/components/channel/ChannelButtons.tsx new file mode 100644 index 0000000000..3b717ca92b --- /dev/null +++ b/src/components/channel/ChannelButtons.tsx @@ -0,0 +1,90 @@ +// External Packages +import { AiOutlinePlus } from 'react-icons/ai'; +import { FiSettings } from 'react-icons/fi'; +import styled from 'styled-components'; + +// Internal Components +import { Button } from 'components/SharedStyling'; + +interface ChannelButtonProps { + onClick: () => void; +} + +interface ModifySettingsButtonProps extends ChannelButtonProps { + title?: string; +} + +export const AddDelegateButton = ({ onClick }: ChannelButtonProps) => { + return ( + + + Add Delegate + + ); +}; + +export const ManageSettingsButton = ({ onClick }: ChannelButtonProps) => { + return ( + + + Manage Settings + + ); +}; + +export const ModifySettingsButton = ({ onClick, title }: ModifySettingsButtonProps) => { + return ( + + {title ? title : 'Modify Settings'} + + ); +}; + +export const AddSettingButton = ({ onClick }: ChannelButtonProps) => { + return ( + + + Add Setting + + ); +}; + +const ChannelButton = styled(Button)` + min-height: 36px; + background: ${(props) => props.theme.default.primaryPushThemeTextColor}; + color: #fff; + z-index: 0; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 17px; + border-radius: 8px; + padding: 4px 12px 4px 12px; +`; + +const ChannelButtonWhite = styled.button` + min-height: 36px; + border: 1px solid ${(props) => props.theme.default.borderColor}; + background: transparent; + color: white; + z-index: 0; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 17px; + border-radius: 8px; + padding: 4px 12px 4px 12px; + cursor: pointer; +`; + +const ButtonText = styled.span` + margin-left: 8px; +`; + +const TransparentButtonText = styled.span` + color: ${(props) => props.theme.default.color}; +`; + +const AddButtonIcon = styled(AiOutlinePlus)` + font-size: 16px; +`; diff --git a/src/components/channel/ChannelInfoHeader.tsx b/src/components/channel/ChannelInfoHeader.tsx new file mode 100644 index 0000000000..c4aec9368e --- /dev/null +++ b/src/components/channel/ChannelInfoHeader.tsx @@ -0,0 +1,69 @@ +// React + Web3 Essentials +import React, { CSSProperties } from 'react'; + +// External Packages +import styled, { useTheme } from 'styled-components'; + +// Internal Compoonents +import { useDeviceWidthCheck } from 'hooks'; +import { Item } from 'primaries/SharedStyling'; +import { Section } from 'components/SharedStyling'; + +// Internal Configs +import { device } from 'config/Globals'; + +interface ChannelInfoHeaderProps { + title: string; + description: string; + Button?: React.ReactNode; + style?: CSSProperties; +} + +const ChannelInfoHeader = ({ title, description, Button, style }: ChannelInfoHeaderProps) => { + const theme = useTheme(); + const isMobile = useDeviceWidthCheck(700); + + return ( + + + {title} + {!isMobile && ( + <> +

+ {description} + + )} + + {Button} + + ); +}; + +export default ChannelInfoHeader; + +const DelegatesInfoHeader = styled.div` + font-weight: 600; + font-size: 18px; + line-height: 150%; + display: flex; + align-items: center; + color: ${(props) => props.theme.color}; +`; + +const DelegatesInfoLabel = styled.div` + font-weight: 400; + font-size: 15px; + line-height: 140%; + color: ${(props) => props.theme.default.secondaryColor}; +`; + +const HeaderSection = styled(Section)` + flex-direction: row; + align-items: center; + padding: 24px 24px 20px 24px; + + @media ${device.tablet} { + padding: 20px 12px; + flex: 0; + } +`; diff --git a/src/components/channel/ChannelInfoList.tsx b/src/components/channel/ChannelInfoList.tsx new file mode 100644 index 0000000000..cfc1a44841 --- /dev/null +++ b/src/components/channel/ChannelInfoList.tsx @@ -0,0 +1,160 @@ +// React + Web3 Essentials +import React, { CSSProperties } from 'react'; + +// External Packages +import styled from 'styled-components'; +import { useNavigate } from 'react-router-dom'; + +// Internal Compoonents +import { Item } from 'primaries/SharedStyling'; +import DelegateInfo from 'components/DelegateInfo'; +import LoaderSpinner, { LOADER_TYPE } from 'components/reusables/loaders/LoaderSpinner'; +import DelegateSettingsDropdown, { ChannelDropdownOption } from './DelegateSettingsDropdown'; +import EmptyNotificationSettings from './EmptyNotificationSettings'; +import Tag from '../reusables/labels/Tag'; + +// Internal Configs +import { device } from 'config/Globals'; +import { ChannelSetting } from 'helpers/channel/types'; + +// Types +interface ChannelInfoListCommonProps { + isLoading: boolean; + account: string; + style?: CSSProperties; +} + +interface AddressListOptions extends ChannelInfoListCommonProps { + isAddress: true; + items: string[]; + addressDropdownOptions: Array; +} + +interface SettingListOptions extends ChannelInfoListCommonProps { + isAddress: false; + items: Array; + isLoading: boolean; + settingsDropdownOptions?: Array; + onClickEmptyListButton: () => void; + emptyListButtonTitle: string; +} + +type ChannelInfoListProps = AddressListOptions | SettingListOptions; + +const ChannelInfoList = (props: ChannelInfoListProps) => { + const isOwner = (account: string, delegate: string) => { + return account.toLowerCase() === delegate.toLowerCase(); + }; + + return ( + + + {props.isLoading ? ( + + + + ) : ( + <> + {props.items && + props.items.length > 0 && + props.items.map((item) => { + return ( +
+ + + + {props.isAddress ? ( + + + + ) : ( + <> + {item.description} + {item.lowerLimit !== undefined && Range} + + )} + {props.isAddress && isOwner(props.account, item) && Creator} + + {props.isAddress === true && + props.addressDropdownOptions?.length > 0 && + !isOwner(props.account, item) && ( + + )} + {props.isAddress === false && props.settingsDropdownOptions?.length > 0 && ( + + )} + +
+ ); + })} + {props.items && props.items.length === 0 && props.isAddress === false && ( + + )} + + )} +
+
+ ); +}; + +export default ChannelInfoList; + +const DelegatesList = styled.div<{ isLoading: boolean }>` + padding: ${(props) => (props.isLoading ? '0px' : '0px 24px 16px')}; + flex: 1; + + @media ${device.tablet} { + flex: 0; + padding: ${(props) => (props.isLoading ? '0px' : '0px 16px 10px')}; + } +`; + +const NotificationSettingName = styled.span` + margin-left: 15px; + color: ${(props) => + props.theme.scheme === 'light' ? props.theme.default.color : props.theme.default.secondaryColor}; +`; + +const Divider = styled.div` + background-color: ${(props) => props.theme.default.border}; + height: 1px; +`; + +const SpinnerContainer = styled.div` + height: 100px; +`; + +const DelegateInfoContainer = styled.div` + @media ${device.tablet} { + margin: 0px 0px 0px 5px; + } +`; diff --git a/src/components/channel/DelegateSettingsDropdown.tsx b/src/components/channel/DelegateSettingsDropdown.tsx new file mode 100644 index 0000000000..dfea1b26a3 --- /dev/null +++ b/src/components/channel/DelegateSettingsDropdown.tsx @@ -0,0 +1,85 @@ +// React + Web3 Essentials +import React, { useRef, useState } from 'react'; + +// External Packages +import { AiOutlineMore } from 'react-icons/ai'; +import styled from 'styled-components'; +import { useClickAway } from 'react-use'; + +export interface ChannelDropdownOption { + icon: React.ReactNode; + text: string; + onClick: (item) => void; +} + +interface DelegateSettingsDropdownProps { + options: Array; + item: string; +} + +const DelegateSettingsDropdown = ({ options, item }: DelegateSettingsDropdownProps) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(); + + useClickAway(dropdownRef, () => setIsOpen(false)); + + return ( +
+ setIsOpen(true)} /> + {isOpen && ( + setIsOpen(false)} ref={dropdownRef}> + {options.map(({ icon, onClick, text }, index) => { + return ( + onClick(item)} + key={index} + index={index} + > + {icon} + {text} + + ); + })} + + )} +
+ ); +}; + +export default DelegateSettingsDropdown; + +const MoreButtonUI = styled(AiOutlineMore)` + background: transparent; + display: flex; + cursor: pointer; + width: 24px; + height: 24px; + padding: 0px; + position: relative; + width: 24px; + height: 24px; + color: ${(props) => props.theme.default.color}; +`; + +const ListContainer = styled.div` + padding: 10px 6px; + width: 119px; + border-radius: 8px; + border: 1px solid ${(props) => props.theme.default.border}; + position: absolute; + top: 3px; + right: 0px; + background-color: ${(props) => props.theme.default.bg}; + z-index: 2; +`; + +const OptionButton = styled.div<{ index: number }>` + cursor: pointer; + display: flex; + flex-direction: row; + margin-top: ${(props) => (props.index === 0 ? '0px' : '16px')}; +`; + +const OptionText = styled.span` + margin-left: 8px; +`; diff --git a/src/components/channel/DepositFeeFooter.tsx b/src/components/channel/DepositFeeFooter.tsx new file mode 100644 index 0000000000..30f49a8480 --- /dev/null +++ b/src/components/channel/DepositFeeFooter.tsx @@ -0,0 +1,309 @@ +// React + Web3 Essentials +import React, { useEffect, useState } from 'react'; + +// External Packages +import styled from 'styled-components'; +import { useSelector } from 'react-redux'; +import { MdCheckCircle, MdError } from 'react-icons/md'; + +// Internal Compoonents +import { ItemHV2, ItemVV2 } from 'components/reusables/SharedStylingV2'; +import FaucetInfo from 'components/FaucetInfo'; +import useToast from 'hooks/useToast'; +import { useAccount } from 'hooks'; + +// Internal Configs +import GLOBALS, { device } from 'config/Globals'; +import { Button } from '../SharedStyling'; +import { LOADER_SPINNER_TYPE } from 'components/reusables/loaders/LoaderSpinner'; +import Spinner from 'components/reusables/spinners/SpinnerUnit'; +import VerifyLogo from '../../assets/Vector.svg'; +import { approvePushToken, getPushTokenApprovalAmount, mintPushToken } from 'helpers'; +import { addresses } from 'config'; + +interface DepositFeeFooterProps { + title: string; + description: string; + onCancel: () => void; + disabled: boolean; + onClick: () => void; + feeRequired: number; +} + +const DepositFeeFooter = ({ title, description, onCancel, disabled, onClick, feeRequired }: DepositFeeFooterProps) => { + const { account, provider } = useAccount(); + const [pushApprovalAmount, setPushApprovalAmount] = useState(0); + const [pushDeposited, setPushDeposited] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const depositFeeToast = useToast(); + + useEffect(() => { + if (!account || !provider) return; + + (async function () { + const pushTokenApprovalAmount = await getPushTokenApprovalAmount({ + address: account, + provider: provider, + contractAddress: addresses.epnscore, + }); + setPushApprovalAmount(parseInt(pushTokenApprovalAmount)); + const amountToBeDeposit = parseInt(pushTokenApprovalAmount); + + if (amountToBeDeposit >= feeRequired && amountToBeDeposit != 0) { + setPushDeposited(true); + } else { + setPushDeposited(false); + } + })(); + }, [account, provider]); + + const depositPush = async () => { + setIsLoading(true); + if (!provider) return; + const signer = provider.getSigner(account); + depositFeeToast.showLoaderToast({ loaderMessage: 'Waiting for Confirmation...' }); + try { + const response = await approvePushToken({ + signer, + contractAddress: addresses.epnscore, + amount: feeRequired - pushApprovalAmount, + }); + console.log('response', response); + if (response) { + setIsLoading(false); + setPushApprovalAmount(feeRequired); + setPushDeposited(true); + depositFeeToast.showMessageToast({ + toastTitle: 'Success', + toastMessage: 'Successfully approved Push!', + toastType: 'SUCCESS', + getToastIcon: (size) => ( + + ), + }); + } + } catch (err) { + console.log(err); + if (err.code == 'ACTION_REJECTED') { + // EIP-1193 userRejectedRequest error + depositFeeToast.showMessageToast({ + toastTitle: 'Error', + toastMessage: `User denied message signature.`, + toastType: 'ERROR', + getToastIcon: (size) => ( + + ), + }); + } else { + depositFeeToast.showMessageToast({ + toastTitle: 'Error', + toastMessage: `There was an error in approving PUSH Token`, + toastType: 'ERROR', + getToastIcon: (size) => ( + + ), + }); + + console.log('Error --> %o', err); + console.log({ err }); + } + } + setIsLoading(false); + }; + + return ( + <> +
+
+ {title} + {description} +
+ + {pushDeposited ? : null} + {feeRequired} PUSH + +
+ { + await mintPushToken({ noOfTokens, provider, account }); + }} + /> + + {isLoading ? ( + <> + {/* Verifying Spinner and Text */} + + + Verifying Transaction + + + ) : ( + <> + {/* This below is Footer Buttons i.e, Cancel and save changes */} + + + Cancel + + + {pushApprovalAmount >= feeRequired ? ( + + Save Changes + + ) : ( + + Approve PUSH + + )} + + + )} + + ); +}; + +export default DepositFeeFooter; + +const TickImage = styled.img``; + +const Footer = styled(ItemVV2)` + background: ${(props) => props.theme.editFooterBg}; + border-radius: 20px; + padding: 23px 32px; + display: grid; + grid-auto-flow: column; + align-content: space-between; + justify-content: space-between; + grid-gap: 40px; + height: 100px; + align-items: center; + z-index: 1; + + @media ${device.tablet} { + padding: 16px; + flex: 0; + } + + @media ${device.mobileL} { + margin: 0px; + } +`; + +const FooterPrimaryText = styled.p` + margin: 0px; + color: ${(props) => props.theme.editChannelPrimaryText}; + font-style: normal; + font-weight: 500; + font-size: 20px; + line-height: 24px; +`; + +const FooterSecondaryText = styled.p` + font-size: 12px; + margin: 0px; + font-weight: 400; + line-height: 130%; + color: ${(props) => props.theme.editChannelSecondaryText}; +`; + +const EditFee = styled.p` + margin: 0px 0px 0px 5px; + color: ${(props) => props.theme.viewChannelSecondaryText}; + font-style: normal; + font-weight: 500; + font-size: 20px; + line-height: 24px; +`; + +const VerifyingContainer = styled(ItemVV2)` + flex-direction: row; + margin-top: 33px; + + @media ${device.tablet} { + flex: 0; + } +`; + +const TransactionText = styled.p` + font-style: normal; + font-weight: 500; + font-size: 18px; + line-height: 22px; + display: flex; + align-items: center; + margin-left: 12px; + color: ${(props) => props.theme.editChannelPrimaryText}; +`; + +const ButtonContainer = styled(ItemHV2)` + justify-content: end; + margin-top: 24px; + @media ${device.mobileL} { + flex-direction: column-reverse; + flex: 0; + } +`; + +const FooterButtons = styled(Button)<{ disabled: boolean }>` + font-style: normal; + font-weight: 500; + font-size: 18px; + line-height: 22px; + display: flex; + border-radius: 15px; + align-items: center; + text-align: center; + background: ${(props) => (props.disabled ? props.theme.nfsDisabled : props.theme.default.primaryPushThemeTextColor)}; + color: ${(props) => (props.disabled ? props.theme.nfsDisabledText : 'white')}; + padding: 16px 27px; + width: 12rem; + + @media ${device.tablet} { + font-size: 15px; + padding: 12px 12px; + width: 8rem; + } + + @media ${device.mobileL} { + width: -webkit-fill-available; + } +`; + +const CancelButtons = styled(FooterButtons)` + margin-right: 14px; + background: ${(props) => props.theme.default.bg}; + color: ${(props) => props.theme.logoBtnColor}; + border: 1px solid ${(props) => + props.theme.scheme === 'light' + ? props.theme.default.primaryPushThemeTextColor + : props.theme.default.borderColor}; + + @media ${device.mobileL} { + margin-right: 0px; + margin-top: 10px; + } +`; diff --git a/src/components/channel/EmptyNotificationSettings.tsx b/src/components/channel/EmptyNotificationSettings.tsx new file mode 100644 index 0000000000..637c115f0b --- /dev/null +++ b/src/components/channel/EmptyNotificationSettings.tsx @@ -0,0 +1,76 @@ +// React + Web3 Essentials +import React from 'react'; + +// External Packages +import styled from 'styled-components'; + +// Internal Components +import Icon from 'assets/navigation/receiveNotifOffIcon.svg'; +import { ImageV2 } from 'components/reusables/SharedStylingV2'; +import { ModifySettingsButton } from './ChannelButtons'; + +// Types +interface EmptyNotificationSettingsProps { + onClick: () => void; + title: string; + description: string; + buttonTitle?: string; + showTopBorder?: boolean; +} + +const EmptyNotificationSettings = ({ + description, + onClick, + title, + buttonTitle, + showTopBorder = true, +}: EmptyNotificationSettingsProps) => { + return ( + + + {title} + {description} + + + ); +}; + +export default EmptyNotificationSettings; + +const EmptyNotificationSetting = styled.div<{ showTopBorder: boolean }>` + border-top: ${(props) => props.showTopBorder && `1px solid ${props.theme.default.borderColor}`}; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + margin-bottom: 16px; +`; + +const EmptyNotificationTitle = styled.div` + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0em; + text-align: left; + color: ${(props) => props.theme.default.color}; +`; + +const EmptyNotificationDesc = styled.div` + margin-top: 1px; + margin-bottom: 16px; + color: ${(props) => props.theme.default.secondaryColor}; +`; + +const NotifIcon = styled(ImageV2)` + color: ${(props) => props.theme.default.color}; + margin-top: 32px; + margin-bottom: 12px; +`; diff --git a/src/components/channel/NotificationSettings.tsx b/src/components/channel/NotificationSettings.tsx new file mode 100644 index 0000000000..4e0cc885b7 --- /dev/null +++ b/src/components/channel/NotificationSettings.tsx @@ -0,0 +1,282 @@ +// React + Web3 Essentials +import React, { useEffect, useMemo } from 'react'; +import { ethers } from 'ethers'; + +// External Packages +import 'react-dropdown/style.css'; +import { useDispatch, useSelector } from 'react-redux'; +import 'react-toastify/dist/ReactToastify.min.css'; +import { useNavigate } from 'react-router-dom'; +import { PiPencilSimpleBold } from 'react-icons/pi'; +import { IoMdRemoveCircleOutline } from 'react-icons/io'; +import { MdCheckCircle, MdError } from 'react-icons/md'; + +// Internal Compoonents +import useToast from '../../hooks/useToast'; +import AddSettingModalContent from './AddSettingModalContent'; +import ChannelInfoHeader from './ChannelInfoHeader'; +import { AddSettingButton } from './ChannelButtons'; +import ChannelInfoList from './ChannelInfoList'; +import DepositFeeFooter from './DepositFeeFooter'; +import { useAccount } from 'hooks'; + +// Internal Configs +import { appConfig } from 'config'; +import useModalBlur, { MODAL_POSITION } from 'hooks/useModalBlur'; +import { ChannelSetting } from 'helpers/channel/types'; +import { getChannel } from 'services'; +import { updateChannelSetting } from 'redux/slices/channelSlice'; + +// Constants +const CORE_CHAIN_ID = appConfig.coreContractChain; + +function NotificationSettings() { + const { account, chainId } = useAccount(); + const { coreChannelAdmin, delegatees } = useSelector((state: any) => state.admin); + const { epnsWriteProvider } = useSelector((state: any) => state.contracts); + const { channelSettings } = useSelector((state: any) => state.channels); + + const dispatch = useDispatch(); + + const onCoreNetwork = CORE_CHAIN_ID === chainId; + const EDIT_SETTING_FEE = 50; + + const [channelAddress, setChannelAddress] = React.useState(''); + const [settings, setSettings] = React.useState([]); + const [settingToEdit, setSettingToEdit] = React.useState(undefined); + const [isLoading, setIsLoading] = React.useState(false); + const [isLoadingSettings, setIsLoadingSettings] = React.useState(true); + + const { + isModalOpen: isAddSettingModalOpen, + showModal: showAddSettingModal, + ModalComponent: AddSettingModal, + } = useModalBlur(); + + const redirectBack = () => { + const url = window.location.origin; + window.location.replace(`${url}/channels`); + }; + + useEffect(() => { + // Is not the channel admin so cannot edit settings + (async () => { + setIsLoading(true); + if (!account) return; + try { + const channelDetails = await getChannel({ channel: account }); + if (!channelDetails) redirectBack(); + } catch { + redirectBack(); + } + if (coreChannelAdmin && coreChannelAdmin !== account) redirectBack(); + setIsLoading(false); + })(); + }, [account, coreChannelAdmin]); + + useEffect(() => { + if (isAddSettingModalOpen === false) setSettingToEdit(undefined); + }, [isAddSettingModalOpen]); + + useEffect(() => { + if (!account) return; + if (!delegatees || !delegatees.length) { + setChannelAddress(account); + } else { + // default the channel address to the first one on the list which should be that of the user if they have a channel + if (onCoreNetwork) setChannelAddress(delegatees[0].channel); + else setChannelAddress(delegatees[0].alias_address); + } + }, [delegatees, account]); + + useEffect(() => { + if (channelAddress && channelSettings[channelAddress]) { + setSettings(channelSettings[channelAddress] || []); + setIsLoadingSettings(false); + } + }, [channelAddress, channelSettings]); + + // Notification Toast + const notificationToast = useToast(5000); + + const navigate = useNavigate(); + + const goBack = () => { + navigate('/dashboard', { replace: true }); + }; + + const addSetting = (newSetting: ChannelSetting) => { + const index = settings.findIndex((setting) => setting.index === newSetting.index); + if (index === -1) setSettings([...settings, newSetting]); + else { + const updatedSetting = [...settings]; + updatedSetting[index] = newSetting; + setSettings(updatedSetting); + } + }; + + const editSetting = (settingToEdit: ChannelSetting) => { + setSettingToEdit(settingToEdit); + showAddSettingModal(); + }; + + const deleteSetting = (settingToDelete: ChannelSetting) => { + setSettings((settings) => settings.filter((setting) => setting.index !== settingToDelete.index)); + }; + + const saveSettings = async () => { + try { + setIsLoading(true); + + const feesRequiredForEdit = 50; + const parsedFees = ethers.utils.parseUnits(feesRequiredForEdit.toString(), 18); + + notificationToast.showLoaderToast({ loaderMessage: 'Waiting for Confirmation...' }); + const notifOptions = settings.length; + let _notifSettings = ''; + let _notifDescription = ''; + settings.forEach((setting) => { + if (_notifSettings !== '') _notifSettings += '+'; + if (_notifDescription !== '') _notifDescription += '+'; + if (setting.type === 1) { + _notifSettings += `${setting.type}-${setting.default ? '1' : '0'}`; + } else if (setting.type === 2) { + _notifSettings += `${setting.type}-${setting.enabled ? '1' : '0'}-${setting.default}-${setting.lowerLimit}-${ + setting.upperLimit + }`; + } + _notifDescription += setting.description; + }); + + const tx = await epnsWriteProvider.createChannelSettings( + notifOptions, + _notifSettings, + _notifDescription, + parsedFees, + { gasLimit: 1000000 } + ); + + console.log(tx); + await tx.wait(); + dispatch(updateChannelSetting({ channelAddress, settings })); + setIsLoading(false); + + notificationToast.showMessageToast({ + toastTitle: 'Success', + toastMessage: `Channel Settings Updated Successfully`, + toastType: 'SUCCESS', + getToastIcon: (size) => ( + + ), + }); + } catch (err) { + setIsLoading(false); + if (err.code == 'ACTION_REJECTED') { + // EIP-1193 userRejectedRequest error + notificationToast.showMessageToast({ + toastTitle: 'Error', + toastMessage: `User denied message signature.`, + toastType: 'ERROR', + getToastIcon: (size) => ( + + ), + }); + } else { + notificationToast.showMessageToast({ + toastTitle: 'Error', + toastMessage: `There was an error in updating channel settings`, + toastType: 'ERROR', + getToastIcon: (size) => ( + + ), + }); + console.log('Error --> %o', err); + } + } + }; + + const settingsChanged = useMemo(() => { + if (!settings || !channelSettings[account]) return false; + if (settings.length !== channelSettings[account].length) return true; + let isUnchanged = true; + for (let i = 0; i < settings.length; i++) { + const setting1 = settings[i]; + const setting2 = channelSettings[account][i]; + if (setting1.type === 1) { + isUnchanged = + isUnchanged && + setting1.type === setting2.type && + setting1.description === setting2.description && + setting1.default === setting2.default; + } else if (setting1.type === 2) { + isUnchanged = + isUnchanged && + setting1.type === setting2.type && + setting1.description === setting2.description && + setting1.default === setting2.default && + setting1.enabled === setting2.enabled && + setting1.lowerLimit === setting2.lowerLimit && + setting1.upperLimit === setting2.upperLimit; + } + } + return isUnchanged === false; + }, [settings, channelSettings[account]]); + + return ( + <> + } + /> + , + onClick: editSetting, + text: 'Edit', + }, + { + icon: , + onClick: deleteSetting, + text: 'Delete', + }, + ]} + /> + + + + ); +} + +// Export Default +export default NotificationSettings; diff --git a/src/components/channel/UserSettings.tsx b/src/components/channel/UserSettings.tsx new file mode 100644 index 0000000000..666486a91d --- /dev/null +++ b/src/components/channel/UserSettings.tsx @@ -0,0 +1,379 @@ +// React + Web3 Essentials +import React, { useEffect, useMemo, useState } from 'react'; + +// External Packages +import styled from 'styled-components'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { AiOutlineMore } from 'react-icons/ai'; +import { cloneDeep } from 'lodash'; + +// Internal Components +import { useAccount } from 'hooks'; +import { Button } from 'primaries/SharedStyling'; +import { ImageV2 } from 'components/reusables/SharedStylingV2'; +import { getChannel, getUserSubscriptions } from 'services'; +import LoaderSpinner from 'primaries/LoaderSpinner'; +import EmptyNotificationSettings from './EmptyNotificationSettings'; +import { updateBulkSubscriptions, updateBulkUserSettings } from 'redux/slices/channelSlice'; +import { convertAddressToAddrCaip } from 'helpers/CaipHelper'; +import ManageNotifSettingDropdown from 'components/dropdowns/ManageNotifSettingDropdown'; + +// Internal Configs +import { device } from 'config/Globals'; + +interface ChannelListItem { + channel: string; + icon: string; + name: string; + id: number; + channel_settings: string; +} + +function UserSettings() { + const { account, chainId } = useAccount(); + const { subscriptionStatus, userSettings: currentUserSettings } = useSelector((state: any) => state.channels); + const [selectedOption, setSelectedOption] = useState(0); + const [channelList, setChannelList] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + const navigate = useNavigate(); + + const dispatch = useDispatch(); + + const fetchChannelDetails = async (channel: string) => { + const details = await getChannel({ channel }); + if (details) { + const updatedChannelItem: ChannelListItem = { + channel, + id: details.id, + icon: details.icon, + name: details.name, + channel_settings: details.channel_settings, + }; + return updatedChannelItem; + } else return undefined; + }; + + const fillData = async (details: any) => { + const data = await Promise.all( + Object.keys(details).map(async (channel) => { + const channelData = await fetchChannelDetails(channel); + if (channelData) return channelData; + }) + ); + setChannelList(data); + }; + + useEffect(() => { + if (!account) return; + (async function () { + setIsLoading(true); + if (Object.keys(subscriptionStatus).length === 0) { + const userCaipAddress = convertAddressToAddrCaip(account, chainId); + const subscriptionsArr = await getUserSubscriptions({ userCaipAddress }); + const subscriptionsMapping = {}; + const userSettings = {}; + subscriptionsArr.map(({ channel, user_settings }) => { + subscriptionsMapping[channel] = true; + userSettings[channel] = user_settings ? JSON.parse(user_settings) : null; + }); + dispatch(updateBulkSubscriptions(subscriptionsMapping)); + dispatch(updateBulkUserSettings(userSettings)); + await fillData(subscriptionsMapping); + } else { + await fillData(subscriptionStatus); + } + setIsLoading(false); + })(); + }, [account]); + + const navigateToChannels = () => { + navigate('/channels'); + }; + + const selectOptions = [ + { + value: 0, + label: 'Notification Settings', + }, + ]; + + const userSettings = useMemo(() => { + return cloneDeep(currentUserSettings); + }, [currentUserSettings]); + + return ( + + Settings + Customize your Push profile or manage your notification preferences + + + {selectOptions.map((selectOptions) => ( + setSelectedOption(selectOptions.value)} + key={selectOptions.value} + isSelected={selectOptions.value === selectedOption} + > + {selectOptions.label} + + ))} + + + + {selectOptions[selectedOption].label} + <> + {isLoading ? ( + + + + ) : ( + <> + {channelList.length > 0 ? ( + channelList.map((channel, index) => ( + <> + {channel && ( + <> + + + + {channel.name} + + { + setChannelList((prevChannelList) => + prevChannelList.filter((item) => item?.id !== channel.id) + ); + }} + > + + + + {index !== channelList.length - 1 &&
} + + )} + + )) + ) : ( + + + + )} + + )} + +
+
+
+
+ ); +} + +// Export Default +export default UserSettings; + +const Container = styled.div` + padding: 32px 24px; + flex: 1; + + @media ${device.tablet} { + padding: 24px 12px; + } +`; + +const PageTitle = styled.div` + font-size: 32px; + font-weight: 500; + line-height: 45px; + letter-spacing: 0em; + text-align: left; + color: ${(props) => props.theme.default.color}; + + @media ${device.tablet} { + text-align: center; + margin-top: 24px; + } +`; + +const PageDescription = styled.div` + font-size: 15px; + font-weight: 400; + line-height: 21px; + letter-spacing: 0em; + text-align: left; + color: ${(props) => props.theme.default.secondaryColor}; + margin-bottom: 40px; + + @media ${device.tablet} { + text-align: center; + margin-bottom: 8px; + } +`; + +const Wrapper = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + + @media ${device.tablet} { + flex-direction: column; + } +`; + +const SelectSection = styled.div` + display: flex; + flex-direction: column; + margin-right: 42px; + + @media ${device.tablet} { + margin-right: 0px; + flex-direction: row; + overflow-x: scroll; + justify-content: center; + } +`; + +const SelectListOption = styled(Button)<{ isSelected: boolean }>` + background-color: ${(props) => (props.isSelected ? props.theme.default.secondaryBg : 'transparent')}; + color: ${(props) => props.theme.default.secondaryColor}; + border-radius: 12px; + width: 100%; + padding: 14px; + margin: 10px 0px; + justify-content: flex-start; + + &:hover:after { + background-color: ${(props) => props.theme.default.secondaryBg}; + } + + @media ${device.tablet} { + padding: 12px; + max-width: fit-content; + } +`; + +const ChannelWrapper = styled.div` + border: 1px solid ${(props) => props.theme.default.borderColor}; + padding: 12px; + border-radius: 16px; + flex-grow: 1; + + @media ${device.tablet} { + margin: 8px 0px; + padding: 12px 6px; + } +`; + +const ChannelContainer = styled.div` + overflow: hidden; + overflow-y: scroll; + height: 55vh; + padding: 12px; + + &::-webkit-scrollbar-track { + background-color: transparent; + position: absolute; + right: 10px; + } + + &::-webkit-scrollbar { + background-color: transparent; + width: 4px; + position: absolute; + right: 10px; + } + + &::-webkit-scrollbar-thumb { + background-color: #d53a94; + border-radius: 99px; + width: 4px; + position: absolute; + right: 10px; + } + + @media ${device.tablet} { + margin: 8px 0px; + padding: 12px 6px; + } +`; + +const SectionTitle = styled.div` + font-size: 22px; + font-weight: 500; + line-height: 33px; + letter-spacing: -0.019em; + text-align: left; + margin-bottom: 20px; + color: ${(props) => props.theme.default.color}; + + @media ${device.tablet} { + text-align: center; + } +`; + +const SettingsListItem = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin: 12px 0px; +`; + +const Icon = styled(ImageV2)` + border: 1px solid ${(props) => props.theme.default.borderColor}; + border-radius: 8px; + overflow: hidden; + margin-right: 16px; + width: 28px; + height: 28px; +`; + +const ChannelName = styled.span` + font-size: 15px; + font-weight: 400; + line-height: 23px; + letter-spacing: 0em; + color: ${(props) => props.theme.default.color}; +`; + +const SettingsListRow = styled.div` + display: flex; + flex-direction: row; + align-items: center; +`; + +const HR = styled.span` + background-color: ${(props) => props.theme.default.borderColor}; + width: 100%; + display: flex; + height: 1px; +`; + +const MoreButtonUI = styled(AiOutlineMore)` + background: transparent; + display: flex; + cursor: pointer; + width: 24px; + height: 24px; + padding: 0px; + position: relative; + width: 24px; + height: 24px; + color: ${(props) => props.theme.default.color}; +`; + +const CenterContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 80%; +`; diff --git a/src/components/dropdowns/DropdownBtnHandler.tsx b/src/components/dropdowns/DropdownBtnHandler.tsx new file mode 100644 index 0000000000..0841041295 --- /dev/null +++ b/src/components/dropdowns/DropdownBtnHandler.tsx @@ -0,0 +1,77 @@ +import React, { useRef } from 'react'; + +// External Packages +import styled, { css } from 'styled-components'; + +// Internal Configs +import { useClickAway } from 'hooks/useClickAway'; +import { ItemHV2 } from 'components/reusables/SharedStylingV2'; + +interface DropdownBtnHandlerProps { + children: React.ReactNode; + renderDropdownContainer: React.ReactNode; + showDropdown: boolean; + toggleDropdown: () => void; + closeDropdown: () => void; + containerPadding?: string; + centerOnMobile: boolean; +} + +export const DropdownBtnHandler: React.FC = ({ + children, + renderDropdownContainer, + showDropdown, + toggleDropdown, + closeDropdown, + containerPadding, + centerOnMobile, +}) => { + const dropdownRef = useRef(null); + const renderDropdownContainerRef = useRef(null); + + useClickAway(dropdownRef, renderDropdownContainerRef, closeDropdown); + + return ( + + {children} + {showDropdown && ( + +
e.stopPropagation()}> + {renderDropdownContainer} +
+
+ )} +
+ ); +}; + +const Container = styled.button` + position:relative; + margin: 0; + padding: 0; + background: none; + border: 0; + outline: 0; +` + +const DropdownContainer = styled(ItemHV2)<{ containerPadding?: string, centerOnMobile: boolean }>` + background: ${(props)=>props.theme.settingsModalBackground}; + border:1px solid; + border-color:${(props)=>props.theme.settingsModalBorderColor}; + border-radius: 8px; + box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.05); + align-items:flex-start; + padding: ${props => props.containerPadding ? props.containerPadding : '7px 14px'}; + position:absolute; + top:0rem; + z-index:10; + right:-0.5rem; + + @media (max-width:768px){ + ${(props) => props.centerOnMobile && css` + left: 50%; + transform: translateX(-50%); + `} + width: fit-content; + } +`; diff --git a/src/components/dropdowns/ManageNotifSettingDropdown.tsx b/src/components/dropdowns/ManageNotifSettingDropdown.tsx new file mode 100644 index 0000000000..00348ececb --- /dev/null +++ b/src/components/dropdowns/ManageNotifSettingDropdown.tsx @@ -0,0 +1,251 @@ +// React + Web3 Essentials +import React, { useContext, useMemo, useState } from "react"; + +// External Packages +import styled, { css, useTheme } from "styled-components"; +import { useDispatch } from "react-redux"; + +// Internal Components +import { DropdownBtnHandler } from "./DropdownBtnHandler"; +import UpdateNotifSettingDropdown from "./UpdateNotifSettingDropdown"; + +// Internal Configs +import { ImageV2, SpanV2 } from "components/reusables/SharedStylingV2"; +import { useAccount } from "hooks"; +import { AppContext } from "contexts/AppContext"; +import useToast from "hooks/useToast"; +import { appConfig } from "config"; +import { MdCheckCircle, MdError } from "react-icons/md"; +import LoaderSpinner, { LOADER_TYPE } from "components/reusables/loaders/LoaderSpinner"; +import { convertAddressToAddrCaip } from "helpers/CaipHelper"; +import { ChannelSetting, UserSetting } from "helpers/channel/types"; +import { removeUserSetting, updateSubscriptionStatus } from "redux/slices/channelSlice"; + +interface ManageNotifSettingDropdownProps { + children: React.ReactNode; + centerOnMobile: boolean; + channelDetail: any; + userSetting?: UserSetting[]; + onSuccessOptout: () => void; +} + +interface ManageNotifSettingDropdownContainerProps { + centerOnMobile: boolean; + userSetting?: UserSetting[]; + channelSetting?: ChannelSetting[]; + channelDetail: any; + optOutHandler: (options: { setLoading?: React.Dispatch> }) => Promise; + closeDropdown: () => void; +} + +const ManageNotifSettingDropdownContainer: React.FC = ({ + centerOnMobile, + optOutHandler, + channelSetting, + channelDetail, + userSetting, + closeDropdown +}) => { + const [txInProgress, setTxInProgress] = useState(false); + + const theme = useTheme(); + + return ( + + {(channelSetting && channelSetting.length != 0) && + + + + + + Manage Settings + + + + +} + optOutHandler({ setLoading: setTxInProgress })}> + + + {txInProgress && + + } + {!txInProgress && Opt-out} + + + + ); +}; + +const ManageNotifSettingDropdown: React.FC = (options) => { + const { + children, + centerOnMobile, + userSetting, + channelDetail, + onSuccessOptout + } = options; + const [isOpen, setIsOpen] = useState(false); + const { chainId } = useAccount(); + const { userPushSDKInstance } = useContext(AppContext); + const dispatch = useDispatch(); + + const channelSetting = useMemo(() => { + if(channelDetail && channelDetail?.channel_settings) { + return JSON.parse(channelDetail?.channel_settings); + } + return null; + }, [channelDetail]); + + const toggleDropdown = () => { + setIsOpen(!isOpen); + }; + + const closeDropdown = () => { + setIsOpen(false); + }; + + const onCoreNetwork = chainId === appConfig.coreContractChain; + + const unsubscribeToast = useToast(); + const optOutHandler = async ({ setLoading }: { setLoading?: React.Dispatch> }) => { + const setLoadingFunc = setLoading || (() => {}); + setLoadingFunc(true); + + try { + let channelAddress = channelDetail.channel; + if (!onCoreNetwork) { + channelAddress = channelDetail.alias_address; + } + + unsubscribeToast.showLoaderToast({ loaderMessage: 'Waiting for Confirmation...' }); + + await userPushSDKInstance.notification.unsubscribe(convertAddressToAddrCaip(channelAddress, chainId), { + onSuccess: () => { + onSuccessOptout(); + dispatch(updateSubscriptionStatus({ channelAddress: channelAddress, status: false })); + dispatch(removeUserSetting(channelAddress)); + + unsubscribeToast.showMessageToast({ + toastTitle: 'Success', + toastMessage: 'Successfully opted out of channel !', + toastType: 'SUCCESS', + getToastIcon: (size) => ( + + ), + }); + + closeDropdown(); + }, + onError: () => { + console.error('opt in error'); + unsubscribeToast.showMessageToast({ + toastTitle: 'Error', + toastMessage: `There was an error opting out of channel`, + toastType: 'ERROR', + getToastIcon: (size) => ( + + ), + }); + }, + }); + } catch (err) { + unsubscribeToast.showMessageToast({ + toastTitle: 'Error', + toastMessage: `There was an error opting into channel ( ${err.message} )`, + toastType: 'ERROR', + getToastIcon: (size) => ( + + ), + }); + + console.log(err); + } finally { + setLoadingFunc(false); + } + }; + + // render + return ( + } + containerPadding="12px 16px" + centerOnMobile={centerOnMobile} + > + {children} + + ); +} + +// Export Default +export default ManageNotifSettingDropdown; + +const DropdownOuterContainer = styled.div` + min-width: max-content; + gap: 16px; + display: flex; + flex-direction: column; +`; + +const DropdownInnerContainer = styled.div` + display: flex; + flex-direction: column; +`; + +const DropdownBtn = styled.button` + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + background: transparent; + cursor: pointer; + gap: 8px; +`; + +const ActionTitle = styled.span<{ hideIt: boolean }>` + ${(props) => + props.hideIt && + css` + visibility: hidden; + `}; +`; \ No newline at end of file diff --git a/src/components/dropdowns/OptinNotifSettingDropdown.tsx b/src/components/dropdowns/OptinNotifSettingDropdown.tsx new file mode 100644 index 0000000000..e145c1d136 --- /dev/null +++ b/src/components/dropdowns/OptinNotifSettingDropdown.tsx @@ -0,0 +1,335 @@ +// React + Web3 Essentials +import React, { useContext, useMemo, useState } from "react"; + +// External Packages +import Switch from 'react-switch'; +import Slider from 'react-input-slider'; +import styled, { css, useTheme } from "styled-components"; +import { useDispatch } from "react-redux"; + +// Internal Components +import { DropdownBtnHandler } from "./DropdownBtnHandler"; + +// Internal Configs +import { SpanV2 } from "components/reusables/SharedStylingV2"; +import { useAccount } from "hooks"; +import { appConfig } from "config"; +import { convertAddressToAddrCaip } from "helpers/CaipHelper"; +import useToast from "hooks/useToast"; +import { MdCheckCircle, MdError } from "react-icons/md"; +import { ChannelSetting } from "helpers/channel/types"; +import { notifChannelSettingFormatString, userSettingsFromDefaultChannelSetting } from "helpers/channel/notifSetting"; +import { AppContext } from "contexts/AppContext"; +import LoaderSpinner, { LOADER_TYPE } from "components/reusables/loaders/LoaderSpinner"; +import { updateSubscriptionStatus, updateUserSetting } from "redux/slices/channelSlice"; + +interface OptinNotifSettingDropdownProps { + children: React.ReactNode; + channelDetail: any; + setLoading: (loading: boolean) => {}; + onSuccessOptin: () => {}; +} + +interface OptinNotifSettingDropdownContainerProps { + settings: ChannelSetting[]; + optInHandler: (options: { channelSettings?: ChannelSetting[], setLoading?: React.Dispatch> }) => Promise; +} + +const OptinNotifSettingDropdownContainer: React.FC = ({ settings, optInHandler }) => { + const [modifiedSettings, setModifiedSettings] = useState([...settings]); + const [txInProgress, setTxInProgress] = useState(false); + + const theme = useTheme(); + + const handleSliderChange = (index: number, value: number) => { + const updatedSettings = [...modifiedSettings]; + updatedSettings[index].default = value; + setModifiedSettings(updatedSettings); + }; + + const handleSwitchChange = (index: number) => { + const updatedSettings = [...modifiedSettings]; + if(updatedSettings[index].type === 1) { + // Type 1 + // Use a type guard to narrow the type to ChannelSetting of type 1 + const setting = updatedSettings[index] as ChannelSetting & { type: 1 }; + setting.default = !setting.default; + } else { + // Type 2 + // Use a type guard to narrow the type to ChannelSetting of type 2 + const setting = updatedSettings[index] as ChannelSetting & { type: 2 }; + setting.enabled = !setting.enabled; + } + setModifiedSettings(updatedSettings); + }; + + return ( + + {modifiedSettings.map((setting, index) => ( + + + {setting.description} + handleSwitchChange(index)} checked={setting.type === 1 ? setting.default : setting.enabled} + checkedIcon={false} + uncheckedIcon={false} + onColor="#D53A94" + offColor="#A0A3B1" + height={16} + width={32} + handleDiameter={12} + /> + + {setting.type === 2 && setting.enabled === true && ( + + handleSliderChange(index, x)} + xstep={1} + xmin={setting.lowerLimit} + xmax={setting.upperLimit} + /> + {setting.default} + + )} + + ))} + + You will receive all important updates from this channel. + optInHandler({ channelSettings: modifiedSettings, setLoading: setTxInProgress })} + > + {txInProgress && + + } + {!txInProgress && Opt-in} + + + + ); +}; + +// Faucet URLs +const OptinNotifSettingDropdown: React.FC = (options) => { + const { children, channelDetail, setLoading, onSuccessOptin } = options; + + const { chainId } = useAccount(); + const { userPushSDKInstance } = useContext(AppContext); + const [isOpen, setIsOpen] = useState(false); + const dispatch = useDispatch(); + + const onCoreNetwork = chainId === appConfig.coreContractChain; + + const channelSetting = useMemo(() => { + if(channelDetail && channelDetail?.channel_settings) { + return JSON.parse(channelDetail?.channel_settings); + } + return null; + }, [channelDetail]); + + const toggleDropdown = () => { + setIsOpen(!isOpen); + }; + + const closeDropdown = () => { + setIsOpen(false); + }; + + const subscribeToast = useToast(); + const optInHandler = async ({ channelSettings, setLoading }: { channelSettings?: ChannelSetting[], setLoading?: React.Dispatch> }) => { + const setLoadingFunc = setLoading || (options && options.setLoading) || (() => {}); + setLoadingFunc(true); + + try { + let channelAddress = channelDetail.channel; + if (!onCoreNetwork) { + channelAddress = channelDetail.alias_address; + } + + subscribeToast.showLoaderToast({ loaderMessage: 'Waiting for Confirmation...' }); + + await userPushSDKInstance.notification.subscribe(convertAddressToAddrCaip(channelAddress, chainId), { + settings: notifChannelSettingFormatString({ settings: channelSettings }), + // settings: [], + onSuccess: () => { + onSuccessOptin(); + dispatch(updateSubscriptionStatus({ channelAddress, status: true })); + dispatch(updateUserSetting({ channelAddress, settings: userSettingsFromDefaultChannelSetting({ channelSetting: channelSettings })})); + + subscribeToast.showMessageToast({ + toastTitle: 'Success', + toastMessage: 'Successfully opted into channel !', + toastType: 'SUCCESS', + getToastIcon: (size) => ( + + ), + }); + }, + onError: () => { + console.error('opt in error'); + subscribeToast.showMessageToast({ + toastTitle: 'Error', + toastMessage: `There was an error opting into channel`, + toastType: 'ERROR', + getToastIcon: (size) => ( + + ), + }); + }, + }); + } catch (err) { + subscribeToast.showMessageToast({ + toastTitle: 'Error', + toastMessage: `There was an error opting into channel ( ${err.message} )`, + toastType: 'ERROR', + getToastIcon: (size) => ( + + ), + }); + + console.log(err); + } finally { + setLoadingFunc(false); + } + }; + + // render + return ( + (channelSetting && channelSetting.length) ? + } + containerPadding="0px 16px 16px 16px" + > + {children} + + : + + {children} + + + ); +} + +// Export Default +export default OptinNotifSettingDropdown; + +const DropdownOuterContainer = styled.div` + min-width: 300px; +`; + +const DropdownInnerContainer = styled.div<{ hasBottomBorder: boolean }>` + display: flex; + flex-direction: column; + min-width: 250px; + + ${(props) => + props.hasBottomBorder && + css` + border-bottom: 1px solid ${(props) => props.theme.settingsModalBorderBottomColor}; + `} +`; + +const DropdownSwitchItem = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0px; +`; + +const DropdownSubmitItem = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0px; +`; + +const DropdownSubmitButton = styled.button` + border: 0; + outline: 0; + display: flex; + align-items: center; + min-width: 90px; + justify-content: center; + margin: 0px 0px 0px 10px; + color: #fff; + font-size: 14px; + font-weight: 400; + position: relative; + background: #e20880; + border-radius: 8px; + padding: 9px 20px; + &:hover { + opacity: 0.9; + cursor: pointer; + pointer: hand; + } + &:active { + opacity: 0.75; + cursor: pointer; + pointer: hand; + } + ${(props) => + props.disabled && + css` + &:hover { + opacity: 1; + cursor: default; + pointer: default; + } + &:active { + opacity: 1; + cursor: default; + pointer: default; + } + `} +` + +const DropdownSliderItem = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 10px; +`; + +const ActionTitle = styled.span<{ hideIt: boolean }>` + ${(props) => + props.hideIt && + css` + visibility: hidden; + `}; +`; diff --git a/src/components/dropdowns/UpdateNotifSettingDropdown.tsx b/src/components/dropdowns/UpdateNotifSettingDropdown.tsx new file mode 100644 index 0000000000..18272d4f72 --- /dev/null +++ b/src/components/dropdowns/UpdateNotifSettingDropdown.tsx @@ -0,0 +1,335 @@ +// React + Web3 Essentials +import React, { useContext, useState } from "react"; + +// External Packages +import Switch from 'react-switch'; +import Slider from 'react-input-slider'; +import styled, { css, useTheme } from "styled-components"; +import { useDispatch } from "react-redux"; + +// Internal Components +import { DropdownBtnHandler } from "./DropdownBtnHandler"; + +// Internal Configs +import { SpanV2 } from "components/reusables/SharedStylingV2"; +import useToast from "hooks/useToast"; +import { useAccount } from "hooks"; +import { AppContext } from "contexts/AppContext"; +import { appConfig } from "config"; +import { ChannelSetting, UserSetting } from "helpers/channel/types"; +import { convertAddressToAddrCaip } from "helpers/CaipHelper"; +import { notifUserSettingFormatString, userSettingsFromDefaultChannelSetting } from "helpers/channel/notifSetting"; +import { MdCheckCircle, MdError } from "react-icons/md"; +import LoaderSpinner, { LOADER_TYPE } from "components/reusables/loaders/LoaderSpinner"; +import { updateUserSetting } from "redux/slices/channelSlice"; + +interface UpdateNotifSettingDropdownProps { + children: React.ReactNode; + centerOnMobile: boolean; + channelDetail: any; + channelSetting?: ChannelSetting[]; + userSetting?: UserSetting[]; + onSuccessSave?: () => void; +} + +interface UpdateNotifSettingDropdownContainerProps { + settings: UserSetting[]; + saveUserSettingHandler: (options: { userSettings?: UserSetting[], setLoading?: React.Dispatch> }) => Promise; +} + +const UpdateNotifSettingDropdownContainer: React.FC = ({ settings, saveUserSettingHandler }) => { + const [modifiedSettings, setModifiedSettings] = useState([...settings]); + const [txInProgress, setTxInProgress] = useState(false); + + const theme = useTheme(); + + const handleSliderChange = (index: number, value: number) => { + const updatedSettings = [...modifiedSettings]; + updatedSettings[index].user = value; + setModifiedSettings(updatedSettings); + }; + + const handleSwitchChange = (index: number) => { + const updatedSettings = [...modifiedSettings]; + if(updatedSettings[index].type === 1) { + // Type 1 + // Use a type guard to narrow the type to UserSetting of type 1 + const setting = updatedSettings[index] as UserSetting & { type: 1 }; + setting.user = !setting.user; + } else { + // Type 2 + // Use a type guard to narrow the type to UserSetting of type 2 + const setting = updatedSettings[index] as UserSetting & { type: 2 }; + setting.enabled = !setting.enabled; + } + setModifiedSettings(updatedSettings); + }; + + return ( + + {modifiedSettings.map((setting, index) => ( + + + {setting.description} + handleSwitchChange(index)} checked={setting.type === 1 ? setting.user : setting.enabled} + checkedIcon={false} + uncheckedIcon={false} + onColor="#D53A94" + offColor="#A0A3B1" + height={16} + width={32} + handleDiameter={12} + /> + + {setting.type === 2 && setting.enabled === true && ( + + handleSliderChange(index, x)} + xstep={1} + xmin={setting.lowerLimit} + xmax={setting.upperLimit} + /> + {setting.user} + + )} + + ))} + + You will receive all important updates from this channel. + saveUserSettingHandler({ userSettings: modifiedSettings, setLoading: setTxInProgress })} + > + {txInProgress && + + } + {!txInProgress && Save} + + + + ); +}; + +// Faucet URLs +const UpdateNotifSettingDropdown: React.FC = ({ + children, + centerOnMobile, + channelDetail, + channelSetting, + userSetting, + onSuccessSave +}) => { + const [isOpen, setIsOpen] = useState(false); + + const { chainId } = useAccount(); + const { userPushSDKInstance } = useContext(AppContext); + const dispatch = useDispatch(); + + const onCoreNetwork = chainId === appConfig.coreContractChain; + + const toggleDropdown = () => { + setIsOpen(!isOpen); + }; + + const closeDropdown = () => { + setIsOpen(false); + }; + + const subscribeToast = useToast(); + const saveUserSettingHandler = async ({ userSettings, setLoading }: { userSettings?: UserSetting[], setLoading?: React.Dispatch> }) => { + const setLoadingFunc = setLoading || (() => {}); + const saveOnSuccessSettingFunc = onSuccessSave || (() => {}); + setLoadingFunc(true); + + try { + let channelAddress = channelDetail.channel; + if (!onCoreNetwork) { + channelAddress = channelDetail.alias_address; + } + + subscribeToast.showLoaderToast({ loaderMessage: 'Waiting for Confirmation...' }); + + await userPushSDKInstance.notification.subscribe(convertAddressToAddrCaip(channelAddress, chainId), { + settings: notifUserSettingFormatString({ settings: userSettings }), + // settings: [], + onSuccess: () => { + saveOnSuccessSettingFunc(); + closeDropdown(); + dispatch(updateUserSetting({ channelAddress, settings: userSetting })); + + subscribeToast.showMessageToast({ + toastTitle: 'Success', + toastMessage: 'Successfully saved the user settings!', + toastType: 'SUCCESS', + getToastIcon: (size) => ( + + ), + }); + }, + onError: () => { + console.error('opt in error'); + subscribeToast.showMessageToast({ + toastTitle: 'Error', + toastMessage: `There was an error in saving the settings`, + toastType: 'ERROR', + getToastIcon: (size) => ( + + ), + }); + }, + }); + } catch (err) { + subscribeToast.showMessageToast({ + toastTitle: 'Error', + toastMessage: `There was an error in saving the settings ( ${err.message} )`, + toastType: 'ERROR', + getToastIcon: (size) => ( + + ), + }); + + console.log(err); + } finally { + setLoadingFunc(false); + } + }; + + // render + return ( + } + containerPadding="0px 16px 16px 16px" + > + {children} + + ); +} + +// Export Default +export default UpdateNotifSettingDropdown; + +const DropdownOuterContainer = styled.div` + min-width: 300px; +`; + +const DropdownInnerContainer = styled.div<{ hasBottomBorder: boolean }>` + display: flex; + flex-direction: column; + min-width: 250px; + + ${(props) => + props.hasBottomBorder && + css` + border-bottom: 1px solid ${(props) => props.theme.settingsModalBorderBottomColor}; + `} +`; + +const DropdownSwitchItem = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0px; +`; + +const DropdownSubmitItem = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0px; +`; + +const DropdownSubmitButton = styled.button` + border: 0; + outline: 0; + display: flex; + align-items: center; + min-width: 90px; + justify-content: center; + margin: 0px 0px 0px 10px; + color: #fff; + font-size: 14px; + font-weight: 400; + position: relative; + background: #e20880; + border-radius: 8px; + padding: 9px 20px; + &:hover { + opacity: 0.9; + cursor: pointer; + pointer: hand; + } + &:active { + opacity: 0.75; + cursor: pointer; + pointer: hand; + } + ${(props) => + props.disabled && + css` + &:hover { + opacity: 1; + cursor: default; + pointer: default; + } + &:active { + opacity: 1; + cursor: default; + pointer: default; + } + `} +` + +const DropdownSliderItem = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 10px; +`; + +const ActionTitle = styled.span<{ hideIt: boolean }>` + ${(props) => + props.hideIt && + css` + visibility: hidden; + `}; +`; diff --git a/src/components/reusables/SharedStylingV2.js b/src/components/reusables/SharedStylingV2.js index fb8af578fb..6e0dfb02e7 100644 --- a/src/components/reusables/SharedStylingV2.js +++ b/src/components/reusables/SharedStylingV2.js @@ -201,6 +201,8 @@ export const ImageV2 = styled.img` export const AInlineV2 = styled.a` background: transparent; + font-size: ${(props) => props.fontSize || 'inherit'}; + font-weight: ${(props) => props.fontWeight || '300'}; color: ${(props) => props.color || '#e1087f'}; display: inline; letter-spacing: inherit; diff --git a/src/components/reusables/labels/Tag.tsx b/src/components/reusables/labels/Tag.tsx new file mode 100644 index 0000000000..ad44e47f72 --- /dev/null +++ b/src/components/reusables/labels/Tag.tsx @@ -0,0 +1,21 @@ +// React + Web3 Essentials +import React from 'react'; + +// External Packages +import styled from 'styled-components'; + +const Tag = ({ children }) => { + return {children}; +}; + +export default Tag; + +const TagLabel = styled.div` + padding: 4px 8px 4px 8px; + border-radius: 4px; + background-color: ${(props) => props.theme.default.secondaryBg}; + color: ${(props) => props.theme.tooltipContentDesc}; + font-size: 10px; + margin-left: 8px; + max-height: 20px; +`; diff --git a/src/config/AppPaths.ts b/src/config/AppPaths.ts new file mode 100644 index 0000000000..1302527e46 --- /dev/null +++ b/src/config/AppPaths.ts @@ -0,0 +1,26 @@ +// Define an enum for the paths +enum APP_PATHS { + Inbox = '/inbox', + Spam = '/spam', + Chat = '/chat', + Spaces = '/spaces', + Channels = '/channels', + Dashboard = '/dashboard', + Send = '/send', + Receive = '/receive', + Govern = '/govern', + Snap = '/snap', + YieldV2 = '/yieldv2', + Rockstar = '/rockstar', + Gratitude = '/gratitude', + LiveWalkthrough = '/live_walkthrough', + ComingSoon = '/comingsoon', + NotAvailable = '/notavailable', + FAQ = '/faq', + Internal = '/internal', + Support = '/support', + UserSettings = '/user/settings', + ChannelSettings = '/channel/settings', +} + +export default APP_PATHS; diff --git a/src/config/NavigationList.js b/src/config/NavigationList.js index 4d0df7653d..fd1b87abef 100644 --- a/src/config/NavigationList.js +++ b/src/config/NavigationList.js @@ -4,6 +4,7 @@ import LoaderSpinner, { LOADER_TYPE } from 'components/reusables/loaders/LoaderS // Internal Configs import GLOBALS from 'config/Globals'; import { themeDark, themeLight } from 'config/Themization'; +import APP_PATHS from './AppPaths'; const NavigationList = { primary: { @@ -14,7 +15,7 @@ const NavigationList = { name: 'Inbox', title: 'Inbox', alt: 'Open Inbox', - href: '/inbox', + href: APP_PATHS.Inbox, newTab: false, isRoute: true, hasMenuLogic: true, @@ -39,7 +40,7 @@ const NavigationList = { name: 'Channels', title: 'Browse Channels', alt: 'Open Channels', - href: '/channels', + href: APP_PATHS.Channels, newTab: false, isRoute: true, hasMenuLogic: true, @@ -65,7 +66,7 @@ const NavigationList = { name: 'Chat', title: 'Chat', alt: 'Open Chat', - href: '/chat', + href: APP_PATHS.Chat, newTab: false, isRoute: true, hasMenuLogic: true, @@ -89,7 +90,7 @@ const NavigationList = { name: 'Spaces', title: 'Spaces', alt: 'Open Spaces', - href: '/spaces', + href: APP_PATHS.Spaces, newTab: false, isRoute: true, hasMenuLogic: true, @@ -116,7 +117,7 @@ const NavigationList = { name: 'Create Channel', title: 'Create Channel', alt: 'Create Channels / Dashboard', - href: '/dashboard', + href: APP_PATHS.Dashboard, newTab: false, isRoute: true, hasMenuLogic: true, @@ -142,7 +143,7 @@ const NavigationList = { name: 'Send Notifications', title: 'Send Notifications', alt: 'Send Notifs', - href: '/send', + href: APP_PATHS.Send, newTab: false, isRoute: true, hasMenuLogic: true, @@ -169,7 +170,7 @@ const NavigationList = { name: 'Yield Farming V2', title: 'Yield Farming V2', alt: 'Open Yield Farming V2', - href: '/yieldv2', + href: APP_PATHS.YieldV2, newTab: false, isRoute: true, hasMenuLogic: true, @@ -244,7 +245,7 @@ const NavigationList = { name: 'Governance', title: 'Governance', alt: 'Governance', - href: '/govern', + href: APP_PATHS.Govern, newTab: false, isRoute: true, hasMenuLogic: true, @@ -398,7 +399,7 @@ const NavigationList = { name: 'FAQs', title: 'Checkout Frequently Asked Questions', alt: 'Open FAQs', - href: '/faq', + href: APP_PATHS.FAQ, newTab: false, isRoute: true, hasMenuLogic: true, @@ -446,7 +447,7 @@ const NavigationList = { name: 'Support', title: 'Open a support ticket', alt: 'Open Support Ticket', - href: '/support', + href: APP_PATHS.Support, newTab: false, opened: false, isRoute: true, diff --git a/src/config/Themization.js b/src/config/Themization.js index ca54836545..f49062faf2 100644 --- a/src/config/Themization.js +++ b/src/config/Themization.js @@ -11,6 +11,7 @@ const themeLight = { secondaryColor: '#657795', hover: '#F3F3FF', borderColor: '#dfdee9', + primaryPushThemeTextColor: '#cf1c84' }, // Login Theme @@ -239,6 +240,14 @@ const themeLight = { activeButtonText:'#657795', stakingBorder:'#BAC4D6', stakingEmptyButtonBG:'#DEDFE1', + + // notif settings modal + settingsModalBorderBottomColor: '#D4DCEA', + sliderActiveColor: '#CF1C84', + sliderTrackColor: '#BAC4D6', + settingsModalPrimaryTextColor: '#1E1E1E', + settingsModalBorderColor: '#D4DFF2', + settingsModalBackground: '#FFF', //spaces spaceHostTextColor: '#1e1e1e', @@ -268,6 +277,14 @@ const themeLight = { snapBackground:'#F2F2F2', snapBorderColor:'#BAC4D6', + // Notification Settings + nfsError: '#ED5858', + nfsDisabled: '#DFDEE9', + nfsDisabledText: '#AFB3BF', + + // Send Notification + snfBorder: '#BAC4D6', + snfToggleBg: '#f4f5fa', }; const themeDark = { @@ -282,6 +299,7 @@ const themeDark = { secondaryColor: '#B6BCD6', hover: '#00000033', borderColor: '#4A4F67', + primaryPushThemeTextColor: '#cf1c84' }, // Login Theme @@ -516,6 +534,14 @@ const themeDark = { emptyButtonText:'#2D313C', emptyButtonBg:'', + // notif settings modal + settingsModalBorderBottomColor: '#4A4F67', + sliderActiveColor: '#CF1C84', + sliderTrackColor: '#4A4F67', + settingsModalPrimaryTextColor: '#fff', + settingsModalBorderColor: '#4A4F67', + settingsModalBackground: '#2F3137', + //spaces spaceHostTextColor: '#ffff', @@ -542,8 +568,16 @@ const themeDark = { snapPrimaryText:'#fff', snapSecondaryText:'#B6BCD6', snapBackground:'#404650', - snapBorderColor:'#787E99' + snapBorderColor:'#787E99', + // Notification Settings + nfsError: '#ED5858', + nfsDisabled: '#AFB3BF', + nfsDisabledText: '#787E99', + + // Send Notification + snfBorder: '#4A4F67', + snfToggleBg: '#404650', }; module.exports = { diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx index cba96971b4..347afb1a74 100644 --- a/src/contexts/AppContext.tsx +++ b/src/contexts/AppContext.tsx @@ -1,14 +1,21 @@ // React + Web3 Essentials import useModalBlur from "hooks/useModalBlur"; -import React,{createContext,useState} from "react" +import React,{createContext,useEffect,useMemo,useState} from "react"; + +// External Packages +import { PushAPI } from "@pushprotocol/restapi"; // Internal Components import { AppContextType, Web3NameListType } from "types/context" +import { appConfig } from "config"; +import { useAccount } from "hooks"; export const AppContext = createContext(null); const AppContextProvider=({children})=>{ const [web3NameList,setWeb3NameList]=useState({}); + const [userPushSDKInstance, setUserPushSDKInstance] = useState(null); + const {account, provider} = useAccount(); const [SnapState, setSnapState] = useState(1); const { @@ -17,10 +24,31 @@ const AppContextProvider=({children})=>{ ModalComponent: MetamaskPushSnapModalComponent, } = useModalBlur(); + useEffect(() => { + const librarySigner = provider?.getSigner(account); + if(!account || !librarySigner || !appConfig?.appEnv) return; + + const initializePushSDK = async () => { + try { + const userInstance = await PushAPI.initialize(librarySigner, { + env: appConfig.appEnv, // defaults to staging + account: account + }); + + setUserPushSDKInstance(userInstance); + } catch (error) { + // Handle initialization error + } + }; + + initializePushSDK(); + }, [account, appConfig?.appEnv, provider]); + return( { + return field.trim().length == 0; +}; + +export const isAllFilledAndValid = ({ + setErrorInfo, + lowerLimit, + upperLimit, + type, + settingName, + defaultValue, +}: { + setErrorInfo: React.Dispatch< + React.SetStateAction<{ + settingName: string; + lowerLimit: string; + upperLimit: string; + default: string; + }> + >; + upperLimit: string; + lowerLimit: string; + type: ChannelSetting['type']; + settingName: string; + defaultValue: string; +}): boolean => { + setErrorInfo(undefined); + + let hasError = false; + + if (isEmpty(settingName)) { + setErrorInfo((x) => ({ + ...x, + settingName: 'Setting Name is required', + })); + hasError = true; + } + + if (type === 2) { + if (isEmpty(lowerLimit)) { + setErrorInfo((x) => ({ + ...x, + lowerLimit: 'Minimum range is required', + })); + hasError = true; + } + if (isEmpty(upperLimit)) { + setErrorInfo((x) => ({ + ...x, + upperLimit: 'Maximum range is required', + })); + hasError = true; + } + if (isEmpty(defaultValue)) { + setErrorInfo((x) => ({ + ...x, + default: 'Default value is required', + })); + hasError = true; + } + if (!isEmpty(lowerLimit) && !isEmpty(upperLimit) && !isEmpty(defaultValue)) { + if (Number(lowerLimit) < 0) { + setErrorInfo((x) => ({ + ...x, + lowerLimit: 'Minimum range should be greater than 0', + })); + hasError = true; + } + if (Number(upperLimit) < 0) { + setErrorInfo((x) => ({ + ...x, + upperLimit: 'Maximum range should be greater than 0', + })); + hasError = true; + } + if (Number(lowerLimit) > Number(upperLimit)) { + setErrorInfo((x) => ({ + ...x, + lowerLimit: 'Minimum range should be less than maximum range', + })); + hasError = true; + } + if (Number(defaultValue) < Number(lowerLimit) || Number(defaultValue) > Number(upperLimit)) { + setErrorInfo((x) => ({ + ...x, + default: 'Default value not in range', + })); + hasError = true; + } + } + } + return !hasError; +}; diff --git a/src/helpers/channel/notifSetting.ts b/src/helpers/channel/notifSetting.ts new file mode 100644 index 0000000000..8daba1d1c9 --- /dev/null +++ b/src/helpers/channel/notifSetting.ts @@ -0,0 +1,30 @@ +import { ChannelSetting, UserSetting } from "./types"; + +const isSettingType1 = (setting: ChannelSetting) => setting.type === 1; + +export const notifChannelSettingFormatString = ({ settings }: { settings: ChannelSetting[] }) => { + let _notifSettings = []; + settings && settings.forEach((setting) => + isSettingType1(setting) + ? _notifSettings.push({ enabled: setting.default }) + : _notifSettings.push({ value: setting.default, enabled: (setting as ChannelSetting & { type: 2 }).enabled })); + return _notifSettings; +} + +export const notifUserSettingFormatString = ({ settings }: { settings: UserSetting[] }) => { + let _notifSettings = []; + settings && settings.forEach((setting) => + isSettingType1(setting) + ? _notifSettings.push({ enabled: setting.user }) + : _notifSettings.push({ value: setting.user, enabled: (setting as ChannelSetting & { type: 2 }).enabled })); + return _notifSettings; +} + +export const userSettingsFromDefaultChannelSetting = ({ channelSetting }: { channelSetting: ChannelSetting[] }) => { + let _userSettings = []; + channelSetting && channelSetting.forEach((setting) => + isSettingType1(setting) + ? _userSettings.push({ ...setting, user: setting.default }) + : _userSettings.push({ ...setting, user: setting.default })); + return _userSettings; +}; \ No newline at end of file diff --git a/src/helpers/channel/types.ts b/src/helpers/channel/types.ts new file mode 100644 index 0000000000..e0f24954b1 --- /dev/null +++ b/src/helpers/channel/types.ts @@ -0,0 +1,36 @@ +export type ChannelSetting = +| { + type: 1; // Boolean + default: boolean; + description: string; + index: number; + } +| { + type: 2; // Range + default: number; + enabled: boolean; + description: string; + index: number; + lowerLimit: number; + upperLimit: number; + }; + +export type UserSetting = +| { + type: 1; // Boolean + default: boolean; + description: string; + index: number; + user: boolean; + } +| { + type: 2; // Range + default: number; + enabled: boolean; + description: string; + index: number; + lowerLimit: number; + upperLimit: number; + user: number; + }; + diff --git a/src/modules/channelDashboard/channelDashboardModule.tsx b/src/modules/channelDashboard/channelDashboardModule.tsx index 7b09a56982..fa0c72da2d 100644 --- a/src/modules/channelDashboard/channelDashboardModule.tsx +++ b/src/modules/channelDashboard/channelDashboardModule.tsx @@ -60,7 +60,7 @@ const Container = styled(Section)` 100% - ${globalsMargin.MINI_MODULES.DESKTOP.RIGHT} - ${globalsMargin.MINI_MODULES.DESKTOP.LEFT} - ${GLOBALS.ADJUSTMENTS.PADDING.HUGE} - ${GLOBALS.ADJUSTMENTS.PADDING.HUGE} ); - padding: ${GLOBALS.ADJUSTMENTS.PADDING.HUGE}; + padding: ${GLOBALS.ADJUSTMENTS.PADDING.DEFAULT}; position: relative; margin: ${GLOBALS.ADJUSTMENTS.MARGIN.MINI_MODULES.DESKTOP}; diff --git a/src/modules/channels/ChannelsModule.tsx b/src/modules/channels/ChannelsModule.tsx index 21c36bcce4..5b902d188e 100644 --- a/src/modules/channels/ChannelsModule.tsx +++ b/src/modules/channels/ChannelsModule.tsx @@ -10,10 +10,11 @@ import ViewChannels from "segments/ViewChannels"; // Internal Configs import GLOBALS, { device, globalsMargin } from "config/Globals"; +import APP_PATHS from "config/AppPaths"; // Create Channels Module const ChannelsModule = ({ loadTeaser, playTeaser }) => { - ReactGA.pageview("/channels"); + ReactGA.pageview(APP_PATHS.Channels); // Render return ( diff --git a/src/modules/editChannel/EditChannel.tsx b/src/modules/editChannel/EditChannel.tsx index ea558e4e05..411d40530c 100644 --- a/src/modules/editChannel/EditChannel.tsx +++ b/src/modules/editChannel/EditChannel.tsx @@ -533,6 +533,7 @@ const Footer = styled(ItemVV2)` justify-content: space-between; grid-gap: 40px; margin-top:35px; + z-index: 1; @media (max-width:600px){ padding: 16px; diff --git a/src/modules/faq/FaqModule.tsx b/src/modules/faq/FaqModule.tsx index 98136d2482..d84f3ba6f6 100644 --- a/src/modules/faq/FaqModule.tsx +++ b/src/modules/faq/FaqModule.tsx @@ -237,7 +237,7 @@ function FaqModule() { Easiest way to create a channel is from our{' '} - + Push (EPNS) Dapp {' '} itself. Find the entire channel creation process{' '} diff --git a/src/modules/inbox/InboxModule.tsx b/src/modules/inbox/InboxModule.tsx index a38666151c..3fa6a3c4ab 100644 --- a/src/modules/inbox/InboxModule.tsx +++ b/src/modules/inbox/InboxModule.tsx @@ -23,6 +23,7 @@ import { useAccount } from 'hooks'; // Internal Configs import { abis, addresses, appConfig, CHAIN_DETAILS } from 'config'; import GLOBALS, { device, globalsMargin } from 'config/Globals'; +import APP_PATHS from 'config/AppPaths'; // Constants export const ALLOWED_CORE_NETWORK = appConfig.coreContractChain; @@ -30,7 +31,7 @@ export const ALLOWED_CORE_NETWORK = appConfig.coreContractChain; // Create Inbox Module const InboxModule = ({isSpam}) => { // React GA Analytics - ReactGA.pageview('/inbox'); + ReactGA.pageview(APP_PATHS.Inbox) const dispatch = useDispatch(); const { account, chainId, provider } = useAccount(); diff --git a/src/modules/notifSettings/NotifSettingsModule.tsx b/src/modules/notifSettings/NotifSettingsModule.tsx new file mode 100644 index 0000000000..59254f1418 --- /dev/null +++ b/src/modules/notifSettings/NotifSettingsModule.tsx @@ -0,0 +1,86 @@ +// React + Web3 Essentials +import React from 'react'; + +// External Packages +import ReactGA from 'react-ga'; +import styled from 'styled-components'; +import { Navigate } from 'react-router-dom'; + +// Internal Components +import NotificationSettings from 'components/channel/NotificationSettings'; +import { Section } from 'primaries/SharedStyling'; + +// Internal Configs +import { appConfig } from 'config'; +import GLOBALS, { device, globalsMargin } from 'config/Globals'; + +// Constants +export const ALLOWED_CORE_NETWORK = appConfig.coreContractChain; //chainId of network which we have deployed the core contract on + +// Create Header +function NotifSettingsPage() { + ReactGA.pageview('/channel/settings'); + + // toast related section + const [toast, showToast] = React.useState(null); + const clearToast = () => showToast(null); + + //clear toast variable after it is shown + React.useEffect(() => { + if (toast) { + clearToast(); + } + }, [toast]); + // toast related section + + // Render + return ( + + + + ); +} + +// Define how the module is fitted, define it align-self to strect to fill entire bounds +// Define height: inherit to cover entire height +const Container = styled(Section)` + align-items: center; + align-self: center; + background: ${(props) => props.theme.default.bg}; + border-radius: ${GLOBALS.ADJUSTMENTS.RADIUS.LARGE} ${GLOBALS.ADJUSTMENTS.RADIUS.LARGE} + ${GLOBALS.ADJUSTMENTS.RADIUS.LARGE} ${GLOBALS.ADJUSTMENTS.RADIUS.LARGE}; + box-shadow: ${GLOBALS.ADJUSTMENTS.MODULE_BOX_SHADOW}; + display: flex; + flex-direction: column; + flex: initial; + justify-content: center; + max-width: 1200px; + width: calc( + 100% - ${globalsMargin.MINI_MODULES.DESKTOP.RIGHT} - ${globalsMargin.MINI_MODULES.DESKTOP.LEFT} - + ${GLOBALS.ADJUSTMENTS.PADDING.BIG} - ${GLOBALS.ADJUSTMENTS.PADDING.BIG} + ); + position: relative; + margin: ${GLOBALS.ADJUSTMENTS.MARGIN.MINI_MODULES.DESKTOP}; + padding: ${GLOBALS.ADJUSTMENTS.PADDING.BIG}; + + @media ${device.laptop} { + margin: ${GLOBALS.ADJUSTMENTS.MARGIN.MINI_MODULES.TABLET}; + padding: ${GLOBALS.ADJUSTMENTS.PADDING.DEFAULT}; + justify-content: flex-start; + } + + @media ${device.mobileL} { + margin: ${GLOBALS.ADJUSTMENTS.MARGIN.BIG_MODULES.MOBILE}; + padding: ${GLOBALS.ADJUSTMENTS.PADDING.DEFAULT}; + width: calc( + 100% - ${globalsMargin.MINI_MODULES.MOBILE.RIGHT} - ${globalsMargin.MINI_MODULES.MOBILE.LEFT} - + ${GLOBALS.ADJUSTMENTS.PADDING.DEFAULT} - ${GLOBALS.ADJUSTMENTS.PADDING.DEFAULT} + ); + min-height: calc(100vh - ${GLOBALS.CONSTANTS.HEADER_HEIGHT}px - ${globalsMargin.BIG_MODULES.MOBILE.TOP}); + overflow-y: scroll; + border-radius: ${GLOBALS.ADJUSTMENTS.RADIUS.LARGE} ${GLOBALS.ADJUSTMENTS.RADIUS.LARGE} 0 0; + } +`; + +// Export Default +export default NotifSettingsPage; diff --git a/src/modules/userSettings/UserSettingsModule.tsx b/src/modules/userSettings/UserSettingsModule.tsx new file mode 100644 index 0000000000..bfb1bcba4e --- /dev/null +++ b/src/modules/userSettings/UserSettingsModule.tsx @@ -0,0 +1,58 @@ +// React + Web3 Essentials +import React from 'react'; + +// External Packages +import styled from 'styled-components'; + +// Internal Compoonents +import UserSettings from 'components/channel/UserSettings'; + +// Internal Configs +import GLOBALS, { device, globalsMargin } from 'config/Globals'; + +// Create Header +const UserSettingsModule = () => { + return ( + + + + ); +} + +// css styles +const Container = styled.div` + align-items: stretch; + align-self: stretch; + flex: 1; + background: ${(props) => props.theme.default.bg}; + border-radius: ${GLOBALS.ADJUSTMENTS.RADIUS.LARGE}; + box-shadow: ${GLOBALS.ADJUSTMENTS.MODULE_BOX_SHADOW}; + display: flex; + flex-direction: column; + flex: initial; + justify-content: center; + position: relative; + overflow: hidden; + box-sizing: border-box; + + margin: ${GLOBALS.ADJUSTMENTS.MARGIN.MINI_MODULES.DESKTOP}; + height: calc(100vh - ${GLOBALS.CONSTANTS.HEADER_HEIGHT}px - ${globalsMargin.MINI_MODULES.DESKTOP.TOP} - ${ + globalsMargin.MINI_MODULES.DESKTOP.BOTTOM +}); + + @media ${device.laptop} { + margin: ${GLOBALS.ADJUSTMENTS.MARGIN.MINI_MODULES.TABLET}; + height: calc(100vh - ${GLOBALS.CONSTANTS.HEADER_HEIGHT}px - ${globalsMargin.MINI_MODULES.TABLET.TOP} - ${ + globalsMargin.MINI_MODULES.TABLET.BOTTOM + }); + } + + @media ${device.mobileL} { + margin: ${GLOBALS.ADJUSTMENTS.MARGIN.MINI_MODULES.MOBILE}; + height: calc(100vh - ${GLOBALS.CONSTANTS.HEADER_HEIGHT}px - ${globalsMargin.MINI_MODULES.MOBILE.TOP}); + border: ${GLOBALS.ADJUSTMENTS.RADIUS.LARGE}; + border-radius: ${GLOBALS.ADJUSTMENTS.RADIUS.LARGE} ${GLOBALS.ADJUSTMENTS.RADIUS.LARGE} 0 0; +`; + +// Export Default +export default UserSettingsModule; diff --git a/src/pages/NotAvailablePage.tsx b/src/pages/NotAvailablePage.tsx index 6b9ad9cdfc..7e8e1982a7 100644 --- a/src/pages/NotAvailablePage.tsx +++ b/src/pages/NotAvailablePage.tsx @@ -11,6 +11,7 @@ import { useAccount } from 'hooks'; // Internal Configs import { appConfig } from "config"; +import APP_PATHS from "config/AppPaths"; function NotAvailablePage(props) { const themes = useTheme(); @@ -20,7 +21,7 @@ function NotAvailablePage(props) { React.useEffect(() => { if (onCoreNetwork) { const url = window.location.origin; - window.location.replace(`${url}/#/channels`); + window.location.replace(`${url}${APP_PATHS.Channels}`); } }) diff --git a/src/pages/NotifSettingsPage.tsx b/src/pages/NotifSettingsPage.tsx new file mode 100644 index 0000000000..1027f064f3 --- /dev/null +++ b/src/pages/NotifSettingsPage.tsx @@ -0,0 +1,29 @@ +// React + Web3 Essentials +import React from "react"; + +// External Packages +import styled from 'styled-components'; + +// Internal Components +import { SectionV2 } from 'components/reusables/SharedStylingV2'; +import NotificationSettings from "modules/notifSettings/NotifSettingsModule"; + +// Page structure +const SendNotifsPage = () => { + // RENDER + return ( + + + + ); +} +export default SendNotifsPage; + +// This defines the page settings, toggle align-self to center if not covering entire stuff, align-items to place them at center +// justify content flex start to start from top, height is defined by module as well as amount of margin, padding +const Container = styled(SectionV2)` + flex: 1; + flex-direction: column; + align-self: stretch; + justify-content: flex-start; +`; \ No newline at end of file diff --git a/src/pages/SpamPage.js b/src/pages/SpamPage.js index 3765ceb60f..3b82ea5913 100644 --- a/src/pages/SpamPage.js +++ b/src/pages/SpamPage.js @@ -7,10 +7,11 @@ import styled from "styled-components"; // Internal Components import Spambox from "segments/Spambox"; +import APP_PATHS from "config/AppPaths"; // Create Header function InboxPage() { - ReactGA.pageview("/spam"); + ReactGA.pageview(APP_PATHS.Spam); // Render return ( diff --git a/src/pages/UserSettingsPage.tsx b/src/pages/UserSettingsPage.tsx new file mode 100644 index 0000000000..435fa15143 --- /dev/null +++ b/src/pages/UserSettingsPage.tsx @@ -0,0 +1,29 @@ +// React + Web3 Essentials +import React from 'react'; + +// External Packages +import styled from 'styled-components'; + +// Internal Components +import { SectionV2 } from 'components/reusables/SharedStylingV2'; +import UserSettingsModule from 'modules/userSettings/UserSettingsModule'; + +// Chat page +const UserSettingsPage = () => { + + // RENDER + return ( + + + + ); +} +export default UserSettingsPage; + +// This defines the page settings, toggle align-self to center if not covering entire stuff, align-items to place them at center +// justify content flex start to start from top, height is defined by module as well as amount of margin, padding +const Container = styled(SectionV2)` + flex: 1; + flex-direction: column; + align-self: stretch; +`; \ No newline at end of file diff --git a/src/primaries/Profile.tsx b/src/primaries/Profile.tsx index ff7f644265..05b4bcb557 100644 --- a/src/primaries/Profile.tsx +++ b/src/primaries/Profile.tsx @@ -12,10 +12,13 @@ import ProfileModal from 'components/ProfileModal'; import Dropdown from '../components/Dropdown'; import { useClickAway } from 'hooks/useClickAway'; import { useResolveWeb3Name } from 'hooks/useResolveWeb3Name'; +import { useAccount } from 'hooks'; + +// Internal Configs +import APP_PATHS from 'config/AppPaths'; import { AppContext } from 'contexts/AppContext'; import { ErrorContext } from 'contexts/ErrorContext'; import { AppContextType } from 'types/context'; -import { useAccount } from 'hooks'; // Create Header const Profile = ({ isDarkMode }) => { @@ -43,6 +46,14 @@ const Profile = ({ isDarkMode }) => { function: ()=>{}, invertedIcon: './copy.svg', }, + { + id: 'userSettings', + value: '', + title: 'Settings', + function: ()=>{}, + to: APP_PATHS.UserSettings, + invertedIcon: 'svg/setting.svg' + }, { id: 'prodDapp', value: '', diff --git a/src/primaries/SharedModalComponents/ModalConfirmButton.tsx b/src/primaries/SharedModalComponents/ModalConfirmButton.tsx index b08ca2fd2f..adb907c004 100644 --- a/src/primaries/SharedModalComponents/ModalConfirmButton.tsx +++ b/src/primaries/SharedModalComponents/ModalConfirmButton.tsx @@ -10,16 +10,17 @@ import LoaderSpinner, { LOADER_TYPE } from 'components/reusables/loaders/LoaderS // Types type ModalConfirmButtonType = { text:string, - onClick: ()=>void, + onClick?: ()=>void, isLoading: boolean, color?:string, backgroundColor?:string, border?:string, topMargin?:string, loaderTitle?: string, + padding?:string, } -const ModalConfirmButton = ({text, onClick, isLoading,color,backgroundColor,border,topMargin,loaderTitle}:ModalConfirmButtonType)=>{ +const ModalConfirmButton = ({text, onClick, isLoading,color,backgroundColor,border,topMargin,loaderTitle,padding}:ModalConfirmButtonType)=>{ const themes = useTheme(); return( @@ -46,6 +47,7 @@ const ModalConfirmButton = ({text, onClick, isLoading,color,backgroundColor,bord color={color} backgroundColor={backgroundColor} border={border} + style={{ padding: padding ? padding : "16px" }} > {text} @@ -87,8 +89,6 @@ const CustomButton = styled.button` background-color:${props => props.backgroundColor || '#CF1C84'}; border:${props=>props.border || '1px solid transparent'}; border-radius:15px; - // padding: 5% 12%; - padding:16px; `; export default ModalConfirmButton \ No newline at end of file diff --git a/src/redux/slices/channelSlice.js b/src/redux/slices/channelSlice.js index 232a4f0496..7356f5797c 100644 --- a/src/redux/slices/channelSlice.js +++ b/src/redux/slices/channelSlice.js @@ -14,6 +14,8 @@ const initialState = { channels: [], // the channels meta-data subscriptionStatus: {}, // a mapping of channel address to user's subscription status channelsCache: {}, // a mapping of channel address to channel details + channelSettings: {}, // a mapping of channel address to channel settings + userSettings: {}, // a mapping of channel address to user settings }; export const channelSlice = createSlice({ @@ -48,7 +50,24 @@ export const channelSlice = createSlice({ updateSubscriptionStatus: (state, action) => { const { channelAddress, status } = action.payload; state.subscriptionStatus[channelAddress] = status; - } + }, + updateBulkUserSettings: (state, action) => { + state.userSettings = action.payload; + }, + updateUserSetting: (state, action) => { + const { channelAddress, settings } = action.payload; + state.userSettings[channelAddress] = settings; + }, + removeUserSetting: (state, action) => { + delete state.userSettings[action.payload]; + }, + updateBulkChannelSettings: (state, action) => { + state.channelSettings = action.payload; + }, + updateChannelSetting: (state, action) => { + const { channelAddress, settings } = action.payload; + state.channelSettings[channelAddress] = settings; + }, }, }); @@ -60,7 +79,12 @@ export const { cacheSubscribe, cacheUnsubscribe, updateBulkSubscriptions, - updateSubscriptionStatus + updateSubscriptionStatus, + updateBulkUserSettings, + updateUserSetting, + removeUserSetting, + updateBulkChannelSettings, + updateChannelSetting, } = channelSlice.actions; export default channelSlice.reducer; diff --git a/src/segments/ViewChannels.tsx b/src/segments/ViewChannels.tsx index 73ea1e2e08..5f293236fa 100644 --- a/src/segments/ViewChannels.tsx +++ b/src/segments/ViewChannels.tsx @@ -12,7 +12,7 @@ import Faucets from 'components/Faucets'; import LoaderSpinner, { LOADER_TYPE } from 'components/reusables/loaders/LoaderSpinner'; import ViewChannelItem from 'components/ViewChannelItem'; import UtilityHelper, { MaskedAliasChannels, MaskedChannels } from 'helpers/UtilityHelper'; -import { incrementPage, setChannelMeta, updateBulkSubscriptions } from 'redux/slices/channelSlice'; +import { incrementPage, setChannelMeta, updateBulkSubscriptions, updateBulkUserSettings } from 'redux/slices/channelSlice'; import { incrementStepIndex } from 'redux/slices/userJourneySlice'; import DisplayNotice from '../primaries/DisplayNotice'; import { Item, ItemH } from '../primaries/SharedStyling'; @@ -196,8 +196,13 @@ function ViewChannels({ loadTeaser, playTeaser }) { const userCaipAddress = convertAddressToAddrCaip(account, chainId); const subscriptionsArr = await getUserSubscriptions({ userCaipAddress }); const subscriptionsMapping = {}; - subscriptionsArr.map(({ channel }) => (subscriptionsMapping[channel] = true)); + const userSettings = {}; + subscriptionsArr.map(({ channel, user_settings }) => { + subscriptionsMapping[channel] = true; + userSettings[channel] = user_settings ? JSON.parse(user_settings) : null; + }); dispatch(updateBulkSubscriptions(subscriptionsMapping)); + dispatch(updateBulkUserSettings(userSettings)); })(); }, [account]); diff --git a/src/services/channels/getChannel.ts b/src/services/channels/getChannel.ts new file mode 100644 index 0000000000..ce3ebd2ebc --- /dev/null +++ b/src/services/channels/getChannel.ts @@ -0,0 +1,19 @@ +// External Packages +import * as PushAPI from '@pushprotocol/restapi'; + +// Internal Configs +import { appConfig } from 'config'; + +// Types +type Props = { + channel: string; +}; + +export const getChannel = async ({ channel }: Props) => { + try { + return await PushAPI.channels.getChannel({ channel, env: appConfig.appEnv }); + } catch (err) { + console.error(err); + throw new Error(err.message); + } +}; diff --git a/src/services/channels/index.ts b/src/services/channels/index.ts index 9073c8ed58..03752f1c6c 100644 --- a/src/services/channels/index.ts +++ b/src/services/channels/index.ts @@ -1,3 +1,4 @@ export * from "./getChannelDelegates"; export * from "./getChannels"; -export * from "./getChannelsSearch"; \ No newline at end of file +export * from "./getChannelsSearch"; +export * from "./getChannel"; diff --git a/src/structure/Header.tsx b/src/structure/Header.tsx index a7dd9c6ad8..5a829816c4 100644 --- a/src/structure/Header.tsx +++ b/src/structure/Header.tsx @@ -28,6 +28,34 @@ import MobileNavigation from './MobileNavigation'; import { useAccount, useDeviceWidthCheck } from 'hooks'; import ChainIndicator from 'components/ChainIndicator'; import { UnsupportedChainIdError } from 'connectors/error'; +import APP_PATHS from 'config/AppPaths'; +import { themeDark, themeLight } from 'config/Themization'; + +// header tags for pages that are not there in navigationList (Sidebar) +const EXTRA_HEADER_TAGS = { + [APP_PATHS.UserSettings]: { + title: 'Settings', + light: { + bg: GLOBALS.COLORS.GRADIENT_PRIMARY, + fg: themeLight.headerTagFg, + }, + dark: { + bg: themeDark.headerTagBg, + fg: themeDark.headerTagFg, + }, + }, + [APP_PATHS.ChannelSettings]: { + title: 'Notification Settings', + light: { + bg: GLOBALS.COLORS.GRADIENT_PRIMARY, + fg: themeLight.headerTagFg, + }, + dark: { + bg: themeDark.headerTagBg, + fg: themeDark.headerTagFg, + }, + } +} // Create Header function Header({ isDarkMode, darkModeToggle }) { @@ -71,6 +99,9 @@ function Header({ isDarkMode, darkModeToggle }) { const item = navigationSetup.navigation[key]; if (location.pathname === item.data.href) { setHeaderTag(item.data.headerTag); + } else { + if(EXTRA_HEADER_TAGS[location.pathname]) + setHeaderTag(EXTRA_HEADER_TAGS[location.pathname]); } }); } diff --git a/src/structure/MasterInterfacePage.tsx b/src/structure/MasterInterfacePage.tsx index b34e610825..f98c251d8f 100644 --- a/src/structure/MasterInterfacePage.tsx +++ b/src/structure/MasterInterfacePage.tsx @@ -23,6 +23,7 @@ const InternalDevPage = lazy(() => import('pages/InternalDevPage')); const NFTPage = lazy(() => import('pages/NFTPage')); const NotAvailablePage = lazy(() => import('pages/NotAvailablePage')); const ReceiveNotifsPage = lazy(() => import('pages/ReceiveNotifsPage')); +const NotifSettingsPage = lazy(() => import('pages/NotifSettingsPage')); const SendNotifsPage = lazy(() => import('pages/SendNotifsPage')); const SpacePage = lazy(() => import('pages/SpacePage')); const SpamPage = lazy(() => import('pages/SpamPage')); @@ -30,6 +31,7 @@ const SupportPage = lazy(() => import('pages/SupportPage')); const TutorialPage = lazy(() => import('pages/TutorialPage')); // const YieldFarmingPage = lazy(() => import('pages/YieldFarmingPage')); const YieldFarmingV2Page = lazy(() => import('pages/YieldFarmingPageV2')); +const UserSettingsPage = lazy(() => import('pages/UserSettingsPage')); // import AirdropPage from 'pages/AirdropPage'; // import ChannelDashboardPage from 'pages/ChannelDashboardPage'; @@ -61,6 +63,7 @@ import { AppContext } from 'contexts/AppContext'; import { AppContextType } from 'types/context'; import MetamaskPushSnapModal from 'modules/receiveNotifs/MetamaskPushSnapModal'; import { MODAL_POSITION } from 'hooks/useModalBlur'; +import APP_PATHS from 'config/AppPaths'; // Create Header function MasterInterfacePage() { @@ -82,36 +85,38 @@ function MasterInterfacePage() { } > - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> {/* } /> */} } /> - } /> - } /> - } /> + } /> + } /> + } /> - } /> - } /> + } /> + } /> {/* } /> */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> diff --git a/src/types/context.ts b/src/types/context.ts index 32e044ac90..576bf0a74a 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -1,3 +1,5 @@ +import { PushAPI } from "@pushprotocol/restapi"; + export interface Web3NameListType { [key: string]: string; } @@ -5,6 +7,7 @@ export interface Web3NameListType { export interface AppContextType { web3NameList: Web3NameListType; setWeb3NameList: (ens: Web3NameListType) => void; + userPushSDKInstance: PushAPI; MetamaskPushSnapModalComponent:any, showMetamaskPushSnap:any, SnapState:number, diff --git a/yarn.lock b/yarn.lock index b10c26aed1..0423a7f5cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -74,6 +74,13 @@ __metadata: languageName: node linkType: hard +"@adraffy/ens-normalize@npm:1.9.4": + version: 1.9.4 + resolution: "@adraffy/ens-normalize@npm:1.9.4" + checksum: 7d7fff58ebe2c4961f7e5e61dad123aa6a63fec0df5c84af1fa41079dc05d398599690be4427b3a94d2baa94084544bcfdf2d51cbed7504b9b0583b0960ad550 + languageName: node + linkType: hard + "@ambire/signature-validator@npm:^1.3.1": version: 1.3.1 resolution: "@ambire/signature-validator@npm:1.3.1" @@ -1687,6 +1694,13 @@ __metadata: languageName: node linkType: hard +"@bufbuild/protobuf@npm:^1.3.0": + version: 1.3.1 + resolution: "@bufbuild/protobuf@npm:1.3.1" + checksum: 5ee96282a6259a222ccd0daf56bcab587918253750dda34c1bf2caad0d89bc3796970f7b55b1c7e25bae5d033c8e9dd687638675fe15cac7d9d6558ebfd4dfa8 + languageName: node + linkType: hard + "@ceramicnetwork/3id-did-resolver@npm:^0.4.11": version: 0.4.11 resolution: "@ceramicnetwork/3id-did-resolver@npm:0.4.11" @@ -2166,7 +2180,7 @@ __metadata: languageName: node linkType: hard -"@emotion/core@npm:^10.0.0, @emotion/core@npm:^10.0.15": +"@emotion/core@npm:^10.0.0, @emotion/core@npm:^10.0.14, @emotion/core@npm:^10.0.15": version: 10.3.1 resolution: "@emotion/core@npm:10.3.1" dependencies: @@ -3663,6 +3677,15 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.4.2": + version: 1.5.0 + resolution: "@floating-ui/core@npm:1.5.0" + dependencies: + "@floating-ui/utils": ^0.1.3 + checksum: 54b4fe26b3c228746ac5589f97303abf158b80aa5f8b99027259decd68d1c2030c4c637648ebd33dfe78a4212699453bc2bd7537fd5a594d3bd3e63d362f666f + languageName: node + linkType: hard + "@floating-ui/dom@npm:^1.0.1": version: 1.2.5 resolution: "@floating-ui/dom@npm:1.2.5" @@ -3672,6 +3695,16 @@ __metadata: languageName: node linkType: hard +"@floating-ui/dom@npm:^1.1.0": + version: 1.5.3 + resolution: "@floating-ui/dom@npm:1.5.3" + dependencies: + "@floating-ui/core": ^1.4.2 + "@floating-ui/utils": ^0.1.3 + checksum: 00053742064aac70957f0bd5c1542caafb3bfe9716588bfe1d409fef72a67ed5e60450d08eb492a77f78c22ed1ce4f7955873cc72bf9f9caf2b0f43ae3561c21 + languageName: node + linkType: hard + "@floating-ui/dom@npm:^1.3.0": version: 1.5.1 resolution: "@floating-ui/dom@npm:1.5.1" @@ -3701,6 +3734,13 @@ __metadata: languageName: node linkType: hard +"@floating-ui/utils@npm:^0.1.3": + version: 0.1.6 + resolution: "@floating-ui/utils@npm:0.1.6" + checksum: b34d4b5470869727f52e312e08272edef985ba5a450a76de0917ba0a9c6f5df2bdbeb99448e2c60f39b177fb8981c772ff1831424e75123471a27ebd5b52c1eb + languageName: node + linkType: hard + "@fontsource/ibm-plex-mono@npm:^4.5.1": version: 4.5.13 resolution: "@fontsource/ibm-plex-mono@npm:4.5.13" @@ -4496,6 +4536,44 @@ __metadata: languageName: node linkType: hard +"@livekit/components-core@npm:0.7.0": + version: 0.7.0 + resolution: "@livekit/components-core@npm:0.7.0" + dependencies: + "@floating-ui/dom": ^1.1.0 + email-regex: ^5.0.0 + global-tld-list: ^0.0.1139 + loglevel: ^1.8.1 + rxjs: ^7.8.0 + peerDependencies: + livekit-client: ^1.12.0 + checksum: 8d225b6160197fa6fe74ec29226d0a58a4eca4e46978617f632a08a6295cbac49b5f1d8f06df89ea1e1476471ae3a141bb1719e142ac4686781ccf8adff7aa94 + languageName: node + linkType: hard + +"@livekit/components-react@npm:^1.2.2": + version: 1.3.0 + resolution: "@livekit/components-react@npm:1.3.0" + dependencies: + "@livekit/components-core": 0.7.0 + "@react-hook/latest": ^1.0.3 + clsx: ^2.0.0 + usehooks-ts: ^2.9.1 + peerDependencies: + livekit-client: ^1.12.0 + react: ">=18" + react-dom: ">=18" + checksum: 1f71f688b6e5aaa4d221ffda67fceb0a9fbeeffb8b9fa9a282d7912ea5b786c1aef688811ad660acd5f18c8d7ec040be21dd28811988e6359acc7196c3fa047d + languageName: node + linkType: hard + +"@livekit/components-styles@npm:^1.0.6": + version: 1.0.6 + resolution: "@livekit/components-styles@npm:1.0.6" + checksum: 5422458a2b442024b38b8b74313e417fd5feaa0b974e416eed45de4feb05b714175ba03ffb8f3cb04d8efd729f26f8f601d96d94d255805891124a84947f620e + languageName: node + linkType: hard + "@livepeer/core-react@npm:^1.8.0": version: 1.8.0 resolution: "@livepeer/core-react@npm:1.8.0" @@ -5104,7 +5182,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:^1.0.0": +"@noble/curves@npm:1.2.0, @noble/curves@npm:^1.0.0, @noble/curves@npm:~1.2.0": version: 1.2.0 resolution: "@noble/curves@npm:1.2.0" dependencies: @@ -5120,7 +5198,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.3.2, @noble/hashes@npm:^1.3.1": +"@noble/hashes@npm:1.3.2, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:~1.3.2": version: 1.3.2 resolution: "@noble/hashes@npm:1.3.2" checksum: fe23536b436539d13f90e4b9be843cc63b1b17666a07634a2b1259dded6f490be3d050249e6af98076ea8f2ea0d56f578773c2197f2aa0eeaa5fba5bc18ba474 @@ -5345,9 +5423,9 @@ __metadata: "@mui/lab": ^5.0.0-alpha.72 "@mui/material": ^5.5.0 "@pushprotocol/ledgerlive": latest - "@pushprotocol/restapi": 1.4.18 + "@pushprotocol/restapi": 0.0.1-alpha.48 "@pushprotocol/socket": latest - "@pushprotocol/uiweb": 1.1.13 + "@pushprotocol/uiweb": 0.0.1-alpha.17 "@reduxjs/toolkit": ^1.7.1 "@testing-library/dom": ^6.12.2 "@testing-library/jest-dom": ^4.2.4 @@ -5424,9 +5502,10 @@ __metadata: react-dropzone-uploader: 2.11.0 react-easy-crop: ^4.1.4 react-ga: 2.7.0 - react-icons: ^4.7.1 + react-icons: ^4.11.0 react-image-file-resizer: ^0.4.7 react-images-upload: ^1.2.8 + react-input-slider: ^6.0.1 react-joyride: ^2.4.0 react-loader-spinner: ^5.3.4 react-multi-select-component: ^4.2.3 @@ -5546,12 +5625,13 @@ __metadata: languageName: node linkType: hard -"@pushprotocol/restapi@npm:1.4.18": - version: 1.4.18 - resolution: "@pushprotocol/restapi@npm:1.4.18" +"@pushprotocol/restapi@npm:0.0.1-alpha.48": + version: 0.0.1-alpha.48 + resolution: "@pushprotocol/restapi@npm:0.0.1-alpha.48" dependencies: "@ambire/signature-validator": ^1.3.1 "@metamask/eth-sig-util": ^5.0.2 + "@pushprotocol/socket": ^0.5.2 axios: ^0.27.2 buffer: ^6.0.3 crypto-js: ^4.1.1 @@ -5560,13 +5640,15 @@ __metadata: livepeer: ^2.5.8 openpgp: ^5.5.0 simple-peer: ^9.11.1 + socket.io-client: ^4.5.2 tslib: ^2.3.0 unique-names-generator: ^4.7.1 uuid: ^9.0.0 video-stream-merger: ^4.0.1 + viem: ^1.3.0 peerDependencies: ethers: ^5.6.8 - checksum: a350b385db72c5ce106ed940d77acfac11cec1960409b18698d63d3359a84788da503b62fa3fd01f94b7a7d3634614e26346432d833eb0d69b717c00bb627415 + checksum: 49aa8e537574b4fbcefa154dbdde36c66bda285a306794dc259f6cbf7e056fdfe9e2d4a1bd3f4bfa0c0f25370b9687e083fabd0113c309e021a0b702ab42dd40 languageName: node linkType: hard @@ -5582,6 +5664,18 @@ __metadata: languageName: node linkType: hard +"@pushprotocol/socket@npm:^0.5.2": + version: 0.5.2 + resolution: "@pushprotocol/socket@npm:0.5.2" + dependencies: + socket.io-client: ^4.5.2 + tslib: ^2.3.0 + peerDependencies: + ethers: ^5.6.8 + checksum: 14a438269eae87979e10377e5b8a38953e190593648ce64f148def553b66467cf52ec29ce2b70956342c23f42b67ae179f54a1aab6f22d1495a3806ba0146bbc + languageName: node + linkType: hard + "@pushprotocol/socket@npm:latest": version: 0.4.2 resolution: "@pushprotocol/socket@npm:0.4.2" @@ -5594,30 +5688,40 @@ __metadata: languageName: node linkType: hard -"@pushprotocol/uiweb@npm:1.1.13": - version: 1.1.13 - resolution: "@pushprotocol/uiweb@npm:1.1.13" +"@pushprotocol/uiweb@npm:0.0.1-alpha.17": + version: 0.0.1-alpha.17 + resolution: "@pushprotocol/uiweb@npm:0.0.1-alpha.17" dependencies: + "@livekit/components-react": ^1.2.2 + "@livekit/components-styles": ^1.0.6 "@livepeer/react": ^2.6.0 "@pushprotocol/socket": ^0.5.0 "@unstoppabledomains/resolution": ^8.5.0 + "@web3-onboard/coinbase": ^2.2.5 + "@web3-onboard/core": ^2.21.1 + "@web3-onboard/injected-wallets": ^2.10.5 + "@web3-onboard/react": ^2.8.9 + "@web3-onboard/walletconnect": ^2.4.6 "@web3-react/injected-connector": ^6.0.7 date-fns: ^2.28.0 emoji-picker-react: ^4.4.9 + ethers: ^5.6.8 font-awesome: ^4.7.0 gif-picker-react: ^1.1.0 html-react-parser: ^1.4.13 + livekit-client: ^1.13.3 moment: ^2.29.4 react-icons: ^4.10.1 react-toastify: ^9.1.3 react-twitter-embed: ^4.0.4 + uuid: ^9.0.1 peerDependencies: "@pushprotocol/restapi": ^1.2.15 "@pushprotocol/socket": ^0.5.0 - ethers: ^5.7.1 + axios: ^0.27.2 react: ">=16.8.0" styled-components: ^5.3.5 - checksum: a03933a5e2c010435aad2bf36657198276b75025521146a64e66847f9f7f492ff85ec4612cf190758d6fac873634feb7250ccf2f3034f399859f478716a96c8e + checksum: 699b576cc1612b41e83790f3e9845fc7702ecf362fb542f28156dadec039f52e41962ab774b61a910c5c05a9afda4ce6d0b79e7d02d33240054f8df230bdad92 languageName: node linkType: hard @@ -6155,6 +6259,15 @@ __metadata: languageName: node linkType: hard +"@react-hook/latest@npm:^1.0.3": + version: 1.0.3 + resolution: "@react-hook/latest@npm:1.0.3" + peerDependencies: + react: ">=16.8" + checksum: 2408c9cd35c5cfa7697b6da3bc5eebef254a932ade70955074c474f23be7dd3e2f81bbba12edcc9208bd0f89c6ed366d6b11d4f6d7b1052877a0bac8f74afad4 + languageName: node + linkType: hard + "@react-spring/animated@npm:~9.7.1": version: 9.7.1 resolution: "@react-spring/animated@npm:9.7.1" @@ -6385,6 +6498,13 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:~1.1.2": + version: 1.1.3 + resolution: "@scure/base@npm:1.1.3" + checksum: 1606ab8a4db898cb3a1ada16c15437c3bce4e25854fadc8eb03ae93cbbbac1ed90655af4b0be3da37e12056fef11c0374499f69b9e658c9e5b7b3e06353c630c + languageName: node + linkType: hard + "@scure/bip32@npm:1.3.1": version: 1.3.1 resolution: "@scure/bip32@npm:1.3.1" @@ -6396,6 +6516,17 @@ __metadata: languageName: node linkType: hard +"@scure/bip32@npm:1.3.2": + version: 1.3.2 + resolution: "@scure/bip32@npm:1.3.2" + dependencies: + "@noble/curves": ~1.2.0 + "@noble/hashes": ~1.3.2 + "@scure/base": ~1.1.2 + checksum: c5ae84fae43490853693b481531132b89e056d45c945fc8b92b9d032577f753dfd79c5a7bbcbf0a7f035951006ff0311b6cf7a389e26c9ec6335e42b20c53157 + languageName: node + linkType: hard + "@scure/bip39@npm:1.2.1": version: 1.2.1 resolution: "@scure/bip39@npm:1.2.1" @@ -7868,6 +7999,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^8.5.5": + version: 8.5.6 + resolution: "@types/ws@npm:8.5.6" + dependencies: + "@types/node": "*" + checksum: 7addb0c5fa4e7713d5209afb8a90f1852b12c02cb537395adf7a05fbaf21205dc5f7c110fd5ad6f3dbf147112cbff33fb11d8633059cb344f0c14f595b1ea1fb + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.0 resolution: "@types/yargs-parser@npm:21.0.0" @@ -8556,6 +8696,30 @@ __metadata: languageName: node linkType: hard +"@walletconnect/core@npm:2.10.1": + version: 2.10.1 + resolution: "@walletconnect/core@npm:2.10.1" + dependencies: + "@walletconnect/heartbeat": 1.2.1 + "@walletconnect/jsonrpc-provider": 1.0.13 + "@walletconnect/jsonrpc-types": 1.0.3 + "@walletconnect/jsonrpc-utils": 1.0.8 + "@walletconnect/jsonrpc-ws-connection": 1.0.13 + "@walletconnect/keyvaluestorage": ^1.0.2 + "@walletconnect/logger": ^2.0.1 + "@walletconnect/relay-api": ^1.0.9 + "@walletconnect/relay-auth": ^1.0.4 + "@walletconnect/safe-json": ^1.0.2 + "@walletconnect/time": ^1.0.2 + "@walletconnect/types": 2.10.1 + "@walletconnect/utils": 2.10.1 + events: ^3.3.0 + lodash.isequal: 4.5.0 + uint8arrays: ^3.1.0 + checksum: d58ae15c53efe1792da8c7aa1b7ba47efb49807cfe0c73f225d59c5cd847a0e50979ce6965b94915812412deba3e5aa2dca13a02bd41c087e85575e99afad223 + languageName: node + linkType: hard + "@walletconnect/core@npm:^1.8.0": version: 1.8.0 resolution: "@walletconnect/core@npm:1.8.0" @@ -8639,6 +8803,28 @@ __metadata: languageName: node linkType: hard +"@walletconnect/ethereum-provider@npm:^2.10.1": + version: 2.10.1 + resolution: "@walletconnect/ethereum-provider@npm:2.10.1" + dependencies: + "@walletconnect/jsonrpc-http-connection": ^1.0.7 + "@walletconnect/jsonrpc-provider": ^1.0.13 + "@walletconnect/jsonrpc-types": ^1.0.3 + "@walletconnect/jsonrpc-utils": ^1.0.8 + "@walletconnect/sign-client": 2.10.1 + "@walletconnect/types": 2.10.1 + "@walletconnect/universal-provider": 2.10.1 + "@walletconnect/utils": 2.10.1 + events: ^3.3.0 + peerDependencies: + "@walletconnect/modal": ">=2" + peerDependenciesMeta: + "@walletconnect/modal": + optional: true + checksum: ec3d88ba101a5d8f193262b5b1e770cccad6457ec56fa1f3d17fa531de4e07e8cf03a1341669122c61956f0d5c3a6eca57d3f12f524e046acddb401cdb76fe7c + languageName: node + linkType: hard + "@walletconnect/events@npm:^1.0.1": version: 1.0.1 resolution: "@walletconnect/events@npm:1.0.1" @@ -8816,6 +9002,15 @@ __metadata: languageName: node linkType: hard +"@walletconnect/modal-core@npm:2.6.2": + version: 2.6.2 + resolution: "@walletconnect/modal-core@npm:2.6.2" + dependencies: + valtio: 1.11.2 + checksum: 94daceba50c323b06ecbeac2968d9f0972f327359c6118887c6526cd64006249b12f64322d71bc6c4a2b928436ecc89cf3d3af706511fcdc264c1f4b34a2dd5d + languageName: node + linkType: hard + "@walletconnect/modal-ui@npm:2.6.1": version: 2.6.1 resolution: "@walletconnect/modal-ui@npm:2.6.1" @@ -8828,6 +9023,18 @@ __metadata: languageName: node linkType: hard +"@walletconnect/modal-ui@npm:2.6.2": + version: 2.6.2 + resolution: "@walletconnect/modal-ui@npm:2.6.2" + dependencies: + "@walletconnect/modal-core": 2.6.2 + lit: 2.8.0 + motion: 10.16.2 + qrcode: 1.5.3 + checksum: cd1ec0205eb491e529670599d3dd26f6782d7c5a99d5594bf6949a8c760c1c5f4eb6ed72b8662450774fe4e2dd47678f2c05145c8f2494bd7153446ddf4bd7ed + languageName: node + linkType: hard + "@walletconnect/modal@npm:2.6.1": version: 2.6.1 resolution: "@walletconnect/modal@npm:2.6.1" @@ -8838,6 +9045,16 @@ __metadata: languageName: node linkType: hard +"@walletconnect/modal@npm:2.6.2": + version: 2.6.2 + resolution: "@walletconnect/modal@npm:2.6.2" + dependencies: + "@walletconnect/modal-core": 2.6.2 + "@walletconnect/modal-ui": 2.6.2 + checksum: 68b354d49960b96d22de0e47a3801df27c01a3e96ec5fbde3ca6df1344ca2b20668b0c4d58fe1803f5670ac7b7b4c6f5b7b405e354f5f9eaff5cca147c13de9c + languageName: node + linkType: hard + "@walletconnect/qrcode-modal@npm:^1.8.0": version: 1.8.0 resolution: "@walletconnect/qrcode-modal@npm:1.8.0" @@ -8921,6 +9138,23 @@ __metadata: languageName: node linkType: hard +"@walletconnect/sign-client@npm:2.10.1": + version: 2.10.1 + resolution: "@walletconnect/sign-client@npm:2.10.1" + dependencies: + "@walletconnect/core": 2.10.1 + "@walletconnect/events": ^1.0.1 + "@walletconnect/heartbeat": 1.2.1 + "@walletconnect/jsonrpc-utils": 1.0.8 + "@walletconnect/logger": ^2.0.1 + "@walletconnect/time": ^1.0.2 + "@walletconnect/types": 2.10.1 + "@walletconnect/utils": 2.10.1 + events: ^3.3.0 + checksum: dbdced8dece73b20ae73df9c0cf0d9e3eee753f6c81e264c87583ca60d1d13d4f7d61944e4b22d1f70c5f32424fd842a7de778838aa7d0ae27195976a86e102f + languageName: node + linkType: hard + "@walletconnect/signer-connection@npm:^1.8.0": version: 1.8.0 resolution: "@walletconnect/signer-connection@npm:1.8.0" @@ -8969,6 +9203,20 @@ __metadata: languageName: node linkType: hard +"@walletconnect/types@npm:2.10.1": + version: 2.10.1 + resolution: "@walletconnect/types@npm:2.10.1" + dependencies: + "@walletconnect/events": ^1.0.1 + "@walletconnect/heartbeat": 1.2.1 + "@walletconnect/jsonrpc-types": 1.0.3 + "@walletconnect/keyvaluestorage": ^1.0.2 + "@walletconnect/logger": ^2.0.1 + events: ^3.3.0 + checksum: b663a236404bb423d3cc5cde656794ce42132f09193da5a51dac815d844f78eebb29c7275ebe10f6134492db21386ffd81b66ce42992332847b72c9128f74990 + languageName: node + linkType: hard + "@walletconnect/types@npm:^1.8.0": version: 1.8.0 resolution: "@walletconnect/types@npm:1.8.0" @@ -8993,6 +9241,23 @@ __metadata: languageName: node linkType: hard +"@walletconnect/universal-provider@npm:2.10.1": + version: 2.10.1 + resolution: "@walletconnect/universal-provider@npm:2.10.1" + dependencies: + "@walletconnect/jsonrpc-http-connection": ^1.0.7 + "@walletconnect/jsonrpc-provider": 1.0.13 + "@walletconnect/jsonrpc-types": ^1.0.2 + "@walletconnect/jsonrpc-utils": ^1.0.7 + "@walletconnect/logger": ^2.0.1 + "@walletconnect/sign-client": 2.10.1 + "@walletconnect/types": 2.10.1 + "@walletconnect/utils": 2.10.1 + events: ^3.3.0 + checksum: a33ad597a7601157cd96bceb7637c3463a5df981e5548c5343ab84f92c542bd7cae577fb2884d549164c9ad8262b097dc5fc0bc7fd9a515ee7c3f30b271cb034 + languageName: node + linkType: hard + "@walletconnect/utils@npm:2.10.0": version: 2.10.0 resolution: "@walletconnect/utils@npm:2.10.0" @@ -9015,6 +9280,28 @@ __metadata: languageName: node linkType: hard +"@walletconnect/utils@npm:2.10.1": + version: 2.10.1 + resolution: "@walletconnect/utils@npm:2.10.1" + dependencies: + "@stablelib/chacha20poly1305": 1.0.1 + "@stablelib/hkdf": 1.0.1 + "@stablelib/random": ^1.0.2 + "@stablelib/sha256": 1.0.1 + "@stablelib/x25519": ^1.0.3 + "@walletconnect/relay-api": ^1.0.9 + "@walletconnect/safe-json": ^1.0.2 + "@walletconnect/time": ^1.0.2 + "@walletconnect/types": 2.10.1 + "@walletconnect/window-getters": ^1.0.1 + "@walletconnect/window-metadata": ^1.0.1 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: ^3.1.0 + checksum: 150d1a3c75ce0736ffc8ed8a844e3dc63476e556f7f308154ee6bc9d99e08907bc11a504b7ce3889951293b48d9eef4e32b84de1c7f27b7a84e6731a7bb65189 + languageName: node + linkType: hard + "@walletconnect/utils@npm:^1.8.0": version: 1.8.0 resolution: "@walletconnect/utils@npm:1.8.0" @@ -9107,6 +9394,38 @@ __metadata: languageName: node linkType: hard +"@web3-onboard/core@npm:^2.21.1": + version: 2.21.2 + resolution: "@web3-onboard/core@npm:2.21.2" + dependencies: + "@web3-onboard/common": ^2.3.3 + bignumber.js: ^9.0.0 + bnc-sdk: ^4.6.7 + bowser: ^2.11.0 + ethers: 5.5.3 + eventemitter3: ^4.0.7 + joi: 17.9.1 + lodash.merge: ^4.6.2 + lodash.partition: ^4.6.0 + nanoid: ^4.0.0 + rxjs: ^7.5.5 + svelte: ^3.49.0 + svelte-i18n: ^3.3.13 + checksum: 13fa0df0c5c8b84cd65e363c2f48f1cd2bcb95217ce570faf87d0e6752add80365fc5554cfb706c375124fcd1e10a0e7321b297f330064574ffb8ac80d16d490 + languageName: node + linkType: hard + +"@web3-onboard/injected-wallets@npm:^2.10.5": + version: 2.10.7 + resolution: "@web3-onboard/injected-wallets@npm:2.10.7" + dependencies: + "@web3-onboard/common": ^2.3.3 + joi: 17.9.1 + lodash.uniqby: ^4.7.0 + checksum: f74617456ec6a5eec45c5c484fc0e3b28d3c267657cb015baf2c53fd23fc1da550b6ce2f71964013b869de190e4bc8534e0753e01891a6dd83c9c3e1529d947a + languageName: node + linkType: hard + "@web3-onboard/injected-wallets@npm:^2.9.0": version: 2.10.5 resolution: "@web3-onboard/injected-wallets@npm:2.10.5" @@ -9147,6 +9466,22 @@ __metadata: languageName: node linkType: hard +"@web3-onboard/walletconnect@npm:^2.4.6": + version: 2.4.7 + resolution: "@web3-onboard/walletconnect@npm:2.4.7" + dependencies: + "@ethersproject/providers": 5.5.0 + "@walletconnect/client": ^1.8.0 + "@walletconnect/ethereum-provider": ^2.10.1 + "@walletconnect/modal": 2.6.2 + "@walletconnect/qrcode-modal": ^1.8.0 + "@web3-onboard/common": ^2.3.3 + joi: 17.9.1 + rxjs: ^7.5.2 + checksum: cfeefcf195ca84fac8de2f80ac8684e980d72c17d519185b2e817b218fa5274839d940b52d6866ae7c6b21b3570726950c84fdd4aee4e3af0a944e6223b1eacd + languageName: node + linkType: hard + "@web3-react/abstract-connector@npm:^6.0.7": version: 6.0.7 resolution: "@web3-react/abstract-connector@npm:6.0.7" @@ -9520,6 +9855,21 @@ __metadata: languageName: node linkType: hard +"abitype@npm:0.9.8": + version: 0.9.8 + resolution: "abitype@npm:0.9.8" + peerDependencies: + typescript: ">=5.0.4" + zod: ^3 >=3.19.1 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + checksum: d7d887f29d6821e3f7a400de9620511b80ead3f85c5c87308aaec97965d3493e6687ed816e88722b4f512249bd66dee9e69231b49af0e1db8f69400a62c87cf6 + languageName: node + linkType: hard + "abort-controller@npm:^3.0.0": version: 3.0.0 resolution: "abort-controller@npm:3.0.0" @@ -11922,6 +12272,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^2.0.0": + version: 2.0.0 + resolution: "clsx@npm:2.0.0" + checksum: a2cfb2351b254611acf92faa0daf15220f4cd648bdf96ce369d729813b85336993871a4bf6978ddea2b81b5a130478339c20d9d0b5c6fc287e5147f0c059276e + languageName: node + linkType: hard + "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -13878,6 +14235,13 @@ __metadata: languageName: node linkType: hard +"email-regex@npm:^5.0.0": + version: 5.0.0 + resolution: "email-regex@npm:5.0.0" + checksum: 4089b601a0db88363391bb5c93f12eba56451687aadc925359b1408831b4b0f281737c51995a6fdbae5ff56f64b2fc2464189107575ecc0d67f0524f457a0632 + languageName: node + linkType: hard + "emittery@npm:^0.10.2": version: 0.10.2 resolution: "emittery@npm:0.10.2" @@ -15309,7 +15673,7 @@ __metadata: languageName: node linkType: hard -"ethers@npm:^5.3.1, ethers@npm:^5.6.5, ethers@npm:^5.7.2": +"ethers@npm:^5.3.1, ethers@npm:^5.6.5, ethers@npm:^5.6.8, ethers@npm:^5.7.2": version: 5.7.2 resolution: "ethers@npm:5.7.2" dependencies: @@ -16433,6 +16797,13 @@ __metadata: languageName: node linkType: hard +"global-tld-list@npm:^0.0.1139": + version: 0.0.1139 + resolution: "global-tld-list@npm:0.0.1139" + checksum: cfe5e6338059328e8b90ef890274419fb718e0c22f9aa0ec88dea016e1a9f756377ba102e1beffdc2d5c3fee3cf9e605cb76e056104f1d8602c599f447aa7d37 + languageName: node + linkType: hard + "global@npm:~4.4.0": version: 4.4.0 resolution: "global@npm:4.4.0" @@ -18411,6 +18782,15 @@ __metadata: languageName: node linkType: hard +"isomorphic-ws@npm:5.0.0": + version: 5.0.0 + resolution: "isomorphic-ws@npm:5.0.0" + peerDependencies: + ws: "*" + checksum: e20eb2aee09ba96247465fda40c6d22c1153394c0144fa34fe6609f341af4c8c564f60ea3ba762335a7a9c306809349f9b863c8beedf2beea09b299834ad5398 + languageName: node + linkType: hard + "isomorphic-ws@npm:^4.0.1": version: 4.0.1 resolution: "isomorphic-ws@npm:4.0.1" @@ -20181,6 +20561,15 @@ __metadata: languageName: node linkType: hard +"lit-html@npm:^2.8.0": + version: 2.8.0 + resolution: "lit-html@npm:2.8.0" + dependencies: + "@types/trusted-types": ^2.0.2 + checksum: 2d70df07248bcb2f502a3afb1e91d260735024fa669669ffb1417575aa39c3092779725ac1b90f5f39e4ce78c63f431f51176bc67f532389f0285a6991573255 + languageName: node + linkType: hard + "lit@npm:2.7.6": version: 2.7.6 resolution: "lit@npm:2.7.6" @@ -20192,6 +20581,32 @@ __metadata: languageName: node linkType: hard +"lit@npm:2.8.0": + version: 2.8.0 + resolution: "lit@npm:2.8.0" + dependencies: + "@lit/reactive-element": ^1.6.0 + lit-element: ^3.3.0 + lit-html: ^2.8.0 + checksum: 2480e733f7d022d3ecba91abc58a20968f0ca8f5fa30b3341ecf4bcf4845e674ad27b721a5ae53529cafc6ca603c015b80d0979ceb7a711e268ef20bb6bc7527 + languageName: node + linkType: hard + +"livekit-client@npm:^1.13.3": + version: 1.13.4 + resolution: "livekit-client@npm:1.13.4" + dependencies: + "@bufbuild/protobuf": ^1.3.0 + events: ^3.3.0 + loglevel: ^1.8.0 + sdp-transform: ^2.14.1 + ts-debounce: ^4.0.0 + typed-emitter: ^2.1.0 + webrtc-adapter: ^8.1.1 + checksum: 90b54ad3dee69bac2f91d09c3db03928448432f8c27ca456717114c891543fab1e2796547322f9a7e3da6047973a0ba36de4b74ed4af66dba0ada25daac228b8 + languageName: node + linkType: hard + "livepeer@npm:2.8.0, livepeer@npm:^2.5.8": version: 2.8.0 resolution: "livepeer@npm:2.8.0" @@ -20474,7 +20889,7 @@ __metadata: languageName: node linkType: hard -"loglevel@npm:^1.7.0": +"loglevel@npm:^1.7.0, loglevel@npm:^1.8.0, loglevel@npm:^1.8.1": version: 1.8.1 resolution: "loglevel@npm:1.8.1" checksum: a1a62db40291aaeaef2f612334c49e531bff71cc1d01a2acab689ab80d59e092f852ab164a5aedc1a752fdc46b7b162cb097d8a9eb2cf0b299511106c29af61d @@ -24812,12 +25227,12 @@ __metadata: languageName: node linkType: hard -"react-icons@npm:^4.7.1": - version: 4.8.0 - resolution: "react-icons@npm:4.8.0" +"react-icons@npm:^4.11.0": + version: 4.11.0 + resolution: "react-icons@npm:4.11.0" peerDependencies: react: "*" - checksum: 4dbba7ad989c295b410e19b2a702722dae44368cb04b6515f9471353552f31ac80bd350f121d5bff79f81504b84039ede44d09e9f035f48bb1032e6eace126c4 + checksum: 7b8b80bbe2dabcc54b6c2129b7761a04b19caebe24389adc7683478ef41212b9ca0b53c63abcc06b3c01b94c84855ec5142b4c357e19c4aaaad9a4db23a3c485 languageName: node linkType: hard @@ -24841,6 +25256,19 @@ __metadata: languageName: node linkType: hard +"react-input-slider@npm:^6.0.1": + version: 6.0.1 + resolution: "react-input-slider@npm:6.0.1" + dependencies: + "@babel/runtime": ^7.9.2 + "@emotion/core": ^10.0.14 + peerDependencies: + react: ">=16" + react-dom: ">=16" + checksum: 8611d1309bc8a10c7181f30c072840548f0911511d10b7c98928eef461297a9c9b645956cadbe0d8e586db4c958cb5e422fbe440d2b422e4cb79e841e4109ef6 + languageName: node + linkType: hard + "react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.6.3, react-is@npm:^16.7.0, react-is@npm:^16.8.0, react-is@npm:^16.8.1, react-is@npm:^16.8.4": version: 16.13.1 resolution: "react-is@npm:16.13.1" @@ -26246,6 +26674,15 @@ __metadata: languageName: node linkType: hard +"rxjs@npm:*, rxjs@npm:^7.5.5, rxjs@npm:^7.8.0": + version: 7.8.1 + resolution: "rxjs@npm:7.8.1" + dependencies: + tslib: ^2.1.0 + checksum: de4b53db1063e618ec2eca0f7965d9137cabe98cf6be9272efe6c86b47c17b987383df8574861bcced18ebd590764125a901d5506082be84a8b8e364bf05f119 + languageName: node + linkType: hard + "rxjs@npm:^6.6.3": version: 6.6.7 resolution: "rxjs@npm:6.6.7" @@ -26264,15 +26701,6 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.5.5": - version: 7.8.1 - resolution: "rxjs@npm:7.8.1" - dependencies: - tslib: ^2.1.0 - checksum: de4b53db1063e618ec2eca0f7965d9137cabe98cf6be9272efe6c86b47c17b987383df8574861bcced18ebd590764125a901d5506082be84a8b8e364bf05f119 - languageName: node - linkType: hard - "sade@npm:^1.8.1": version: 1.8.1 resolution: "sade@npm:1.8.1" @@ -26508,6 +26936,15 @@ __metadata: languageName: node linkType: hard +"sdp-transform@npm:^2.14.1": + version: 2.14.1 + resolution: "sdp-transform@npm:2.14.1" + bin: + sdp-verify: checker.js + checksum: 8b3179786db1a0f1ebfdacb1ac0dfe2833e63e8c64b638884cec212455061d53beaa8d9c8bf76fdbd5f844b7885f3892adec27e87734cfbc2b3e5c65e18a489b + languageName: node + linkType: hard + "sdp@npm:^2.12.0, sdp@npm:^2.6.0": version: 2.12.0 resolution: "sdp@npm:2.12.0" @@ -26515,6 +26952,13 @@ __metadata: languageName: node linkType: hard +"sdp@npm:^3.2.0": + version: 3.2.0 + resolution: "sdp@npm:3.2.0" + checksum: 227885bddab9a5845e56ae184ff51e43ec7bc155e7f1ed2f17ca1b012e6767011d5bd01b6c4064ded8e3b6f6bf3c9b26b2cf754b9c8662285988ed27b54f37b1 + languageName: node + linkType: hard + "secp256k1-v4@https://github.com/HarshRajat/secp256k1-node": version: 4.0.1 resolution: "secp256k1-v4@https://github.com/HarshRajat/secp256k1-node.git#commit=90a04a2e1127f4c1bfd7015aa5a7b22d08edb811" @@ -28482,6 +28926,13 @@ __metadata: languageName: node linkType: hard +"ts-debounce@npm:^4.0.0": + version: 4.0.0 + resolution: "ts-debounce@npm:4.0.0" + checksum: e1e509632c5aa09c40d3fa315b3a95b2c2e8813ccc706a400aa08e41f691e658061f34b42a1e8a578a043540d6db198e6ecf3ce26a5356a02a0940985fb1e379 + languageName: node + linkType: hard + "ts-easing@npm:^0.2.0": version: 0.2.0 resolution: "ts-easing@npm:0.2.0" @@ -28725,6 +29176,18 @@ __metadata: languageName: node linkType: hard +"typed-emitter@npm:^2.1.0": + version: 2.1.0 + resolution: "typed-emitter@npm:2.1.0" + dependencies: + rxjs: "*" + dependenciesMeta: + rxjs: + optional: true + checksum: 95821a9e05784b972cc9d152891fd12a56cb4b1a7c57e768c02bea6a8984da7aff8f19404a7b69eea11fae2a3b6c0c510a4c510f575f50162c759ae9059f2520 + languageName: node + linkType: hard + "typedarray-to-buffer@npm:3.1.5, typedarray-to-buffer@npm:^3.1.5, typedarray-to-buffer@npm:~3.1.5": version: 3.1.5 resolution: "typedarray-to-buffer@npm:3.1.5" @@ -29118,6 +29581,16 @@ __metadata: languageName: node linkType: hard +"usehooks-ts@npm:^2.9.1": + version: 2.9.1 + resolution: "usehooks-ts@npm:2.9.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 36f1e4142ce23bc019b81d2e93aefd7f2c350abcf255598c21627114a69a2f2f116b35dc3a353375f09c6e4c9b704a04f104e3d10e98280545c097feca66c30a + languageName: node + linkType: hard + "utf-8-validate@npm:^5.0.2, utf-8-validate@npm:^5.0.8": version: 5.0.10 resolution: "utf-8-validate@npm:5.0.10" @@ -29224,6 +29697,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^9.0.1": + version: 9.0.1 + resolution: "uuid@npm:9.0.1" + bin: + uuid: dist/bin/uuid + checksum: 39931f6da74e307f51c0fb463dc2462807531dc80760a9bff1e35af4316131b4fc3203d16da60ae33f07fdca5b56f3f1dd662da0c99fea9aaeab2004780cc5f4 + languageName: node + linkType: hard + "v8-compile-cache@npm:^2.0.3": version: 2.3.0 resolution: "v8-compile-cache@npm:2.3.0" @@ -29257,6 +29739,24 @@ __metadata: languageName: node linkType: hard +"valtio@npm:1.11.2": + version: 1.11.2 + resolution: "valtio@npm:1.11.2" + dependencies: + proxy-compare: 2.5.1 + use-sync-external-store: 1.2.0 + peerDependencies: + "@types/react": ">=16.8" + react: ">=16.8" + peerDependenciesMeta: + "@types/react": + optional: true + react: + optional: true + checksum: cce2d9212aac9fc4bdeba2d381188cc831cfe8d2d03039024cfcd58ba1801f2a5b14d01c2bb21a2c9f12046d2ede64f1dd887175185f39bee553677a35592c30 + languageName: node + linkType: hard + "varint@npm:^5.0.0, varint@npm:^5.0.2, varint@npm:~5.0.0": version: 5.0.2 resolution: "varint@npm:5.0.2" @@ -29296,6 +29796,28 @@ __metadata: languageName: node linkType: hard +"viem@npm:^1.3.0": + version: 1.15.4 + resolution: "viem@npm:1.15.4" + dependencies: + "@adraffy/ens-normalize": 1.9.4 + "@noble/curves": 1.2.0 + "@noble/hashes": 1.3.2 + "@scure/bip32": 1.3.2 + "@scure/bip39": 1.2.1 + "@types/ws": ^8.5.5 + abitype: 0.9.8 + isomorphic-ws: 5.0.0 + ws: 8.13.0 + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: 66fe2d8c00504e308b18d730dfd5bb7139e7b8a8c7fe10d8170c3bc4b33b25a9862dd09c7d4fac095310053a898dea18f850b72fcc5b408a439866c0e85d3f94 + languageName: node + linkType: hard + "w3c-hr-time@npm:^1.0.2": version: 1.0.2 resolution: "w3c-hr-time@npm:1.0.2" @@ -29843,6 +30365,15 @@ __metadata: languageName: node linkType: hard +"webrtc-adapter@npm:^8.1.1": + version: 8.2.3 + resolution: "webrtc-adapter@npm:8.2.3" + dependencies: + sdp: ^3.2.0 + checksum: 8239c9452c489c9aad2584b5d00af22462c3e0f1b7885c6e4036b518d2b9411d94c00d2ceadbed987459a3647cfc4ce04c0eb75dd5ae7c3d7df9b810525e6a07 + languageName: node + linkType: hard + "websocket-driver@npm:>=0.5.1, websocket-driver@npm:^0.7.4": version: 0.7.4 resolution: "websocket-driver@npm:0.7.4" @@ -30335,6 +30866,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:8.13.0, ws@npm:^8.13.0": + version: 8.13.0 + resolution: "ws@npm:8.13.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 53e991bbf928faf5dc6efac9b8eb9ab6497c69feeb94f963d648b7a3530a720b19ec2e0ec037344257e05a4f35bd9ad04d9de6f289615ffb133282031b18c61c + languageName: node + linkType: hard + "ws@npm:^3.0.0": version: 3.3.3 resolution: "ws@npm:3.3.3" @@ -30361,21 +30907,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.13.0": - version: 8.13.0 - resolution: "ws@npm:8.13.0" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 53e991bbf928faf5dc6efac9b8eb9ab6497c69feeb94f963d648b7a3530a720b19ec2e0ec037344257e05a4f35bd9ad04d9de6f289615ffb133282031b18c61c - languageName: node - linkType: hard - "ws@npm:^8.5.0": version: 8.14.2 resolution: "ws@npm:8.14.2"