From d5ff18b41b6e4fd91d76d5ffd76fa4cddd60ace3 Mon Sep 17 00:00:00 2001 From: kalashshah <202051096@iiitvadodara.ac.in> Date: Tue, 3 Oct 2023 17:20:35 +0530 Subject: [PATCH 01/10] chore: upgrade react icons --- package.json | 2 +- yarn.lock | 26 ++++++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 1347c86350..5085ea406d 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "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", diff --git a/yarn.lock b/yarn.lock index b10c26aed1..e66c89183d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2166,7 +2166,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: @@ -5424,9 +5424,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 @@ -24812,12 +24813,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 +24842,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" From 3e2c5c9b960ae14658e0c8de4787118238e3ee6f Mon Sep 17 00:00:00 2001 From: kalashshah <202051096@iiitvadodara.ac.in> Date: Tue, 3 Oct 2023 17:23:04 +0530 Subject: [PATCH 02/10] feat: components for notif setting --- .../channel/AddSettingModalContent.tsx | 369 ++++++++++++++++++ src/components/channel/ChannelButtons.tsx | 94 +++++ src/components/channel/ChannelInfoHeader.tsx | 69 ++++ src/components/channel/ChannelInfoList.tsx | 206 ++++++++++ .../channel/DelegateSettingsDropdown.tsx | 85 ++++ src/components/channel/DepositFeeFooter.tsx | 324 +++++++++++++++ .../channel/NotificationSettings.tsx | 277 +++++++++++++ src/helpers/channel/InputValidation.ts | 96 +++++ src/helpers/channel/types.ts | 15 + 9 files changed, 1535 insertions(+) create mode 100644 src/components/channel/AddSettingModalContent.tsx create mode 100644 src/components/channel/ChannelButtons.tsx create mode 100644 src/components/channel/ChannelInfoHeader.tsx create mode 100644 src/components/channel/ChannelInfoList.tsx create mode 100644 src/components/channel/DelegateSettingsDropdown.tsx create mode 100644 src/components/channel/DepositFeeFooter.tsx create mode 100644 src/components/channel/NotificationSettings.tsx create mode 100644 src/helpers/channel/InputValidation.ts create mode 100644 src/helpers/channel/types.ts diff --git a/src/components/channel/AddSettingModalContent.tsx b/src/components/channel/AddSettingModalContent.tsx new file mode 100644 index 0000000000..df363676d3 --- /dev/null +++ b/src/components/channel/AddSettingModalContent.tsx @@ -0,0 +1,369 @@ +// 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.default === 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.default ? 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), + 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; +`; + +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` + 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 rgba(237, 88, 88, 1)` : `1px solid ${props.theme.default.borderColor}`}; +`; + +const InputWithError = styled(Input)<{ hasError: boolean }>` + flex: 1; + border: ${(props) => + props.hasError ? `1px solid rgba(237, 88, 88, 1)` : `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: rgba(237, 88, 88, 1); + 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..72d6e5d3a8 --- /dev/null +++ b/src/components/channel/ChannelButtons.tsx @@ -0,0 +1,94 @@ +// 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; +} + +export const AddDelegateButton = ({ onClick }: ChannelButtonProps) => { + return ( + + + Add Delegate + + ); +}; + +export const ManageSettingsButton = ({ onClick }: ChannelButtonProps) => { + return ( + + + Manage Settings + + ); +}; + +export const ModifySettingsButton = ({ onClick }: ChannelButtonProps) => { + return ( + + Modify Settings + + ); +}; + +export const AddSettingButton = ({ onClick }: ChannelButtonProps) => { + return ( + + + Add Setting + + ); +}; + +const ChannelButton = styled(Button)` + height: 36px; + background: #cf1c84; + color: #fff; + z-index: 0; + font-family: 'Strawford'; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 17px; + border-radius: 8px; + padding: 4px 12px 4px 12px; + @media (min-width: 600px) and (max-width: 700px) { + margin-right: 9px; + } +`; + +const ChannelButtonWhite = styled.button` + height: 36px; + border: 1px solid ${(props) => props.theme.default.borderColor}; + background: transparent; + color: #fff; + z-index: 0; + font-family: 'Strawford'; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 17px; + border-radius: 8px; + padding: 4px 12px 4px 12px; + cursor: pointer; + @media (min-width: 600px) and (max-width: 700px) { + margin-right: 9px; + } +`; + +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..3294760cc4 --- /dev/null +++ b/src/components/channel/ChannelInfoList.tsx @@ -0,0 +1,206 @@ +// 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 Icon from 'assets/navigation/receiveNotifOffIcon.svg'; +import { ImageV2 } from 'components/reusables/SharedStylingV2'; +import { ModifySettingsButton } from './ChannelButtons'; +import DelegateSettingsDropdown, { ChannelDropdownOption } from './DelegateSettingsDropdown'; + +// Internal Configs +import { device } from 'config/Globals'; +import { ChannelSetting } from 'helpers/channel/types'; + +const isOwner = (account: string, delegate: string) => { + return account.toLowerCase() === delegate.toLowerCase(); +}; + +type ChannelInfoListProps = + | { + isAddress: true; + items: string[]; + isLoading: boolean; + account: string; + style?: CSSProperties; + addressDropdownOptions: Array; + } + | { + isAddress: false; + items: Array; + isLoading: boolean; + account: string; + style?: CSSProperties; + settingsDropdownOptions?: Array; + }; + +const ChannelInfoList = (props: ChannelInfoListProps) => { + const navigate = useNavigate(); + + const handleNavigateToModifySettings = () => { + navigate(`/notif-settings`); + }; + + 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 && ( + + + No settings added + + Add settings for users to customize their notification preferences. + + + + )} + + )} +
+
+ ); +}; + +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 Tag = styled.div` + padding: 4px 8px 4px 8px; + border-radius: 4px; + background-color: rgba(244, 245, 250, 1); + color: rgba(73, 77, 95, 1); + font-size: 10px; + margin-left: 8px; +`; + +const NotificationSettingName = styled.span` + margin-left: 15px; +`; + +const EmptyNotificationSetting = styled.div` + border-top: ${(props) => `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-family: Strawford; + 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; +`; + +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..c53141066d --- /dev/null +++ b/src/components/channel/DepositFeeFooter.tsx @@ -0,0 +1,324 @@ +// 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; +} + +const DepositFeeFooter = ({ title, description, onCancel, disabled, onClick }: DepositFeeFooterProps) => { + const { account, provider } = useAccount(); + const { epnsReadProvider } = useSelector((state: any) => state.contracts); + const [feesRequiredForEdit, setFeesRequiredForEdit] = useState(0); + const [pushApprovalAmount, setPushApprovalAmount] = useState(0); + const [pushDeposited, setPushDeposited] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + // it can be fetched from contract for dynamic, but making it const will be fast + const minFees = 50; + + const depositFeeToast = useToast(); + + useEffect(() => { + if (!account) return; + + (async function () { + const amount = await epnsReadProvider.channelUpdateCounter(account); + setFeesRequiredForEdit(minFees * (Number(amount) + 1)); + })(); + }, [account]); + + 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 >= feesRequiredForEdit && 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: feesRequiredForEdit - pushApprovalAmount, + }); + console.log('response', response); + if (response) { + setIsLoading(false); + setPushApprovalAmount(feesRequiredForEdit); + 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} + {feesRequiredForEdit} 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 >= feesRequiredForEdit ? ( + + 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-family: 'Strawford'; + 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: #d53893; + font-family: 'Strawford'; + 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-family: 'Strawford'; + 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)` + font-family: 'Strawford'; + font-style: normal; + font-weight: 500; + font-size: 18px; + line-height: 22px; + display: flex; + border-radius: 15px; + align-items: center; + text-align: center; + background: #cf1c84; + color: #fff; + 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 #CF1C84; + @media (max-width:425px){ + margin-right:0px; + margin-top:10px; + } + + &:hover{ + color:#AC106C; + border:border: 1px solid #AC106C; + background:transparent; + opacity:1; + } + + &:after{ + background:white; + } +`; diff --git a/src/components/channel/NotificationSettings.tsx b/src/components/channel/NotificationSettings.tsx new file mode 100644 index 0000000000..30026ce5d0 --- /dev/null +++ b/src/components/channel/NotificationSettings.tsx @@ -0,0 +1,277 @@ +// React + Web3 Essentials +import React, { useEffect, useMemo } from 'react'; +import { ethers } from 'ethers'; + +// External Packages +import 'react-dropdown/style.css'; +import { 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'; + +// Constants +const CORE_CHAIN_ID = appConfig.coreContractChain; + +function NotificationSettings() { + const { account, chainId } = useAccount(); + const { + coreChannelAdmin, + channelDetails, + delegatees, + aliasDetails: { aliasEthAddr }, + } = useSelector((state: any) => state.admin); + const { epnsWriteProvider } = useSelector((state: any) => state.contracts); + + const onCoreNetwork = CORE_CHAIN_ID === chainId; + + const [channelAddress, setChannelAddress] = React.useState(''); + const [settings, setSettings] = React.useState([]); + const [settingToEdit, setSettingToEdit] = React.useState(undefined); + const [isLoading, setIsLoading] = React.useState(false); + const [currentSettings, setCurrentSettings] = React.useState([]); + const [isLoadingSettings, setIsLoadingSettings] = React.useState(true); + + const { + isModalOpen: isAddSettingModalOpen, + showModal: showAddSettingModal, + ModalComponent: AddSettingModal, + } = useModalBlur(); + + useEffect(() => { + // Is not the channel admin so cannot edit settings + setIsLoading(true); + if (coreChannelAdmin && account && coreChannelAdmin !== account) { + const url = window.location.origin; + window.location.replace(`${url}/channels`); + } + setIsLoading(false); + }, [account, coreChannelAdmin]); + + useEffect(() => { + if (isAddSettingModalOpen === false) setSettingToEdit(undefined); + }, [isAddSettingModalOpen]); + + 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 (delegatees) { + const delegatee = delegatees.find(({ channel }) => channel === channelAddress); + if (delegatee) { + const { channel_settings } = delegatee; + if (channel_settings !== null) { + const settings = JSON.parse(channel_settings); + setSettings(settings); + setCurrentSettings(settings); + setIsLoadingSettings(false); + } + } + } + return null; + }, [delegatees, channelAddress]); + + // 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 = ethers.utils.parseUnits('2', 18); + 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.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(); + setCurrentSettings(settings); + setIsLoading(false); + + notificationToast.showMessageToast({ + toastTitle: 'Success', + toastMessage: `Channel Settings Updated Successfully`, + toastType: 'SUCCESS', + getToastIcon: (size) => ( + + ), + }); + } catch (err) { + setIsLoading(false); + console.log(err.message); + 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); + console.log({ err }); + } + } + }; + + const settingsChanged = useMemo(() => { + if (!settings || !currentSettings) return false; + console.log('Settings changed bro', settings, currentSettings); + if (settings.length !== currentSettings.length) return true; + let isUnchanged = true; + for (let i = 0; i < settings.length; i++) { + const setting1 = settings[i]; + const setting2 = currentSettings[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.lowerLimit === setting2.lowerLimit && + setting1.upperLimit === setting2.upperLimit; + } + } + return isUnchanged === false; + }, [settings, currentSettings]); + + return ( + <> + } + /> + , + onClick: editSetting, + text: 'Edit', + }, + { + icon: , + onClick: deleteSetting, + text: 'Delete', + }, + ]} + /> + + + + ); +} + +// Export Default +export default NotificationSettings; diff --git a/src/helpers/channel/InputValidation.ts b/src/helpers/channel/InputValidation.ts new file mode 100644 index 0000000000..eaa0d7843e --- /dev/null +++ b/src/helpers/channel/InputValidation.ts @@ -0,0 +1,96 @@ +import { ChannelSetting } from './types'; + +const isEmpty = (field: string) => { + 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; + } + } + } + console.log('Has error', hasError); + return !hasError; +}; diff --git a/src/helpers/channel/types.ts b/src/helpers/channel/types.ts new file mode 100644 index 0000000000..09616ad0bc --- /dev/null +++ b/src/helpers/channel/types.ts @@ -0,0 +1,15 @@ +export type ChannelSetting = + | { + type: 1; // Boolean + default: boolean; + description: string; + index: number; + } + | { + type: 2; // Range + default: number; + description: string; + index: number; + lowerLimit: number; + upperLimit: number; + }; From e1cd50fc3f261027a8fd9aa37d581d9b6493c64e Mon Sep 17 00:00:00 2001 From: kalashshah <202051096@iiitvadodara.ac.in> Date: Tue, 3 Oct 2023 17:26:29 +0530 Subject: [PATCH 03/10] chore: remove add/delete delegate from modal --- src/components/ChannelSettingsDropdown.tsx | 73 +---- src/components/RemoveDelegateModalContent.tsx | 112 ------- src/components/ShowDelegates.jsx | 280 ------------------ 3 files changed, 1 insertion(+), 464 deletions(-) delete mode 100644 src/components/RemoveDelegateModalContent.tsx delete mode 100644 src/components/ShowDelegates.jsx 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 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/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; From bb93d245703bdb3a576635134be50ab0c6f9fae5 Mon Sep 17 00:00:00 2001 From: kalashshah <202051096@iiitvadodara.ac.in> Date: Tue, 3 Oct 2023 17:28:06 +0530 Subject: [PATCH 04/10] feat: notif settings page structure --- .../notifSettings/NotifSettingsModule.tsx | 86 +++++++++++++++++++ src/pages/NotifSettingsPage.tsx | 29 +++++++ src/structure/MasterInterfacePage.tsx | 2 + 3 files changed, 117 insertions(+) create mode 100644 src/modules/notifSettings/NotifSettingsModule.tsx create mode 100644 src/pages/NotifSettingsPage.tsx diff --git a/src/modules/notifSettings/NotifSettingsModule.tsx b/src/modules/notifSettings/NotifSettingsModule.tsx new file mode 100644 index 0000000000..330d6d6843 --- /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('/notif-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/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/structure/MasterInterfacePage.tsx b/src/structure/MasterInterfacePage.tsx index b34e610825..d472bd59be 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')); @@ -97,6 +98,7 @@ function MasterInterfacePage() { } /> } /> } /> + } /> } /> } /> From c2f1e0258ab665f2d66d0ade14745990ecdb9e6a Mon Sep 17 00:00:00 2001 From: kalashshah <202051096@iiitvadodara.ac.in> Date: Tue, 3 Oct 2023 17:28:58 +0530 Subject: [PATCH 05/10] chore: update modal confirm button --- .../SharedModalComponents/ModalConfirmButton.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 From 3191facc31740e1d3e4e181dd3ef0bd0623feaec Mon Sep 17 00:00:00 2001 From: kalashshah <202051096@iiitvadodara.ac.in> Date: Tue, 3 Oct 2023 17:30:20 +0530 Subject: [PATCH 06/10] feat: channel details page --- src/components/ChannelDetails.js | 368 +++++++++++++++++++++++-------- src/components/DelegateInfo.js | 59 +++-- 2 files changed, 310 insertions(+), 117 deletions(-) diff --git a/src/components/ChannelDetails.js b/src/components/ChannelDetails.js index 5378c6e53a..352a897dae 100644 --- a/src/components/ChannelDetails.js +++ b/src/components/ChannelDetails.js @@ -1,38 +1,45 @@ // 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"; + 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 { CHANNEL_ACTIVE_STATE, CHANNNEL_DEACTIVATED_STATE } = useSelector((state) => state.channels); @@ -40,7 +47,7 @@ export default function ChannelDetails({ isChannelExpired, setIsChannelExpired, 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 +55,43 @@ export default function ChannelDetails({ isChannelExpired, setIsChannelExpired, const onCoreNetwork = CORE_CHAIN_ID === chainId; const isMobile = useDeviceWidthCheck(600); + 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 +99,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 +109,138 @@ 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]); + + const channelSettings = useMemo(() => { + if (delegatees) { + const delegatee = delegatees.find(({ channel }) => channel === channelAddress); + if (delegatee) { + const { channel_settings } = delegatee; + if (channel_settings !== null) { + return JSON.parse(channel_settings); + } + } + } + return []; + }, [delegatees, channelAddress]); + + const delegateeList = useMemo(() => { + if (delegatees) { + return delegatees.map(({ channel, alias_address }) => { + return onCoreNetwork ? channel : alias_address; + }); + } + return []; + }); + + const removeDelegate = (walletAddress) => { + return epnsCommWriteProvider.removeDelegate(walletAddress); + }; + + const navigateToNotifSettings = () => { + navigate('/notif-settings'); + }; 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 +249,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} + - {channelDetails.info} - - {canVerify && - + {canVerify && ( + verified by: @@ -169,41 +300,99 @@ export default function ChannelDetails({ isChannelExpired, setIsChannelExpired, - } - - {processingState === 0 && + )} + + {processingState === 0 && ( - +
+ + navigateToNotifSettings()} />} + /> + + +
+
+ + 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 +428,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 +457,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 +508,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 +538,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 +569,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 +587,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 +598,15 @@ const SubmitButton = styled(Button)` margin-right: 9px; border-radius: 8px; padding: 10px 16px; - -`; \ No newline at end of file +`; + +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}; +`; diff --git a/src/components/DelegateInfo.js b/src/components/DelegateInfo.js index 297491830a..8fd2071194 100644 --- a/src/components/DelegateInfo.js +++ b/src/components/DelegateInfo.js @@ -7,10 +7,9 @@ import { RiFileCopyFill, RiFileCopyLine } from "react-icons/ri"; // Internal Components import { useDeviceWidthCheck } from "hooks"; -import { Item } from "primaries/SharedStyling"; import { shortenText } from "helpers/UtilityHelper"; -const DelegateInfo = ({ delegateAddress, isDelegate, maxWidth }) => { +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; From 57c345d9d85f2896f169c378c90bdce69068925c Mon Sep 17 00:00:00 2001 From: kalashshah <202051096@iiitvadodara.ac.in> Date: Tue, 3 Oct 2023 17:30:40 +0530 Subject: [PATCH 07/10] chore: ui fixes --- src/components/ChannelReactivateModalContent.tsx | 2 +- src/components/FaucetInfo.tsx | 5 ++++- src/components/SendNotifications.tsx | 2 +- src/components/SharedStyling.js | 2 -- src/components/StakingInfo.tsx | 4 ++-- src/modules/channelDashboard/channelDashboardModule.tsx | 2 +- src/modules/editChannel/EditChannel.tsx | 1 + 7 files changed, 10 insertions(+), 8 deletions(-) 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/FaucetInfo.tsx b/src/components/FaucetInfo.tsx index 0f7f91be66..d511bbb399 100644 --- a/src/components/FaucetInfo.tsx +++ b/src/components/FaucetInfo.tsx @@ -110,7 +110,8 @@ const FaucetInfo = ({ onMintPushToken, noOfPushTokensToCheck, containerProps }: }; const Container = styled.div` -width:100%; + width:100%; + transform: translateY(-40px); ` const TextSpace = styled.div` @@ -125,6 +126,8 @@ const TextSpace = styled.div` border-radius: 0px 0px 28px 28px; padding: 32px 32px 20px 32px; margin-top:24px; + margin-bottom: -40px; + @media ${device.tablet} { width: 100%; flex-direction: column; diff --git a/src/components/SendNotifications.tsx b/src/components/SendNotifications.tsx index 676cd842ba..9c2be83280 100644 --- a/src/components/SendNotifications.tsx +++ b/src/components/SendNotifications.tsx @@ -17,7 +17,7 @@ import styled, { useTheme } from 'styled-components'; import * as PushAPI from '@pushprotocol/restapi'; import { postReq } from 'api'; import LoaderSpinner, { LOADER_TYPE } from 'components/reusables/loaders/LoaderSpinner'; -import { SectionV2, AInlineV2 } from 'components/reusables/SharedStylingV2'; +import { AInlineV2, SectionV2 } from 'components/reusables/SharedStylingV2'; import { convertAddressToAddrCaip } from 'helpers/CaipHelper'; import CryptoHelper from 'helpers/CryptoHelper'; import { IPFSupload } from 'helpers/IpfsHelper'; 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/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/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/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; From a43adab3f3f5af6ad7041a94fa5179df870d744e Mon Sep 17 00:00:00 2001 From: kalashshah <202051096@iiitvadodara.ac.in> Date: Tue, 3 Oct 2023 17:34:33 +0530 Subject: [PATCH 08/10] chore: update url to notif settings page --- src/components/ChannelDetails.js | 2 +- src/components/channel/ChannelInfoList.tsx | 2 +- src/modules/notifSettings/NotifSettingsModule.tsx | 2 +- src/structure/MasterInterfacePage.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ChannelDetails.js b/src/components/ChannelDetails.js index 352a897dae..9f02e70b8d 100644 --- a/src/components/ChannelDetails.js +++ b/src/components/ChannelDetails.js @@ -147,7 +147,7 @@ export default function ChannelDetails({ isChannelExpired, setIsChannelExpired, }; const navigateToNotifSettings = () => { - navigate('/notif-settings'); + navigate('/channel/settings'); }; return ( diff --git a/src/components/channel/ChannelInfoList.tsx b/src/components/channel/ChannelInfoList.tsx index 3294760cc4..12a97af4a8 100644 --- a/src/components/channel/ChannelInfoList.tsx +++ b/src/components/channel/ChannelInfoList.tsx @@ -44,7 +44,7 @@ const ChannelInfoList = (props: ChannelInfoListProps) => { const navigate = useNavigate(); const handleNavigateToModifySettings = () => { - navigate(`/notif-settings`); + navigate(`/channel/settings`); }; return ( diff --git a/src/modules/notifSettings/NotifSettingsModule.tsx b/src/modules/notifSettings/NotifSettingsModule.tsx index 330d6d6843..59254f1418 100644 --- a/src/modules/notifSettings/NotifSettingsModule.tsx +++ b/src/modules/notifSettings/NotifSettingsModule.tsx @@ -19,7 +19,7 @@ export const ALLOWED_CORE_NETWORK = appConfig.coreContractChain; //chainId of ne // Create Header function NotifSettingsPage() { - ReactGA.pageview('/notif-settings'); + ReactGA.pageview('/channel/settings'); // toast related section const [toast, showToast] = React.useState(null); diff --git a/src/structure/MasterInterfacePage.tsx b/src/structure/MasterInterfacePage.tsx index d472bd59be..56f0ead8df 100644 --- a/src/structure/MasterInterfacePage.tsx +++ b/src/structure/MasterInterfacePage.tsx @@ -98,7 +98,7 @@ function MasterInterfacePage() { } /> } /> } /> - } /> + } /> } /> } /> From 5ae98de584ca16c6b75e257de20adf72b67312f1 Mon Sep 17 00:00:00 2001 From: kalashshah <202051096@iiitvadodara.ac.in> Date: Thu, 5 Oct 2023 14:28:22 +0530 Subject: [PATCH 09/10] feat: get channel service --- src/services/channels/getChannel.ts | 19 +++++++++++++++++++ src/services/channels/index.ts | 3 ++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/services/channels/getChannel.ts 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"; From 1ea7ae02db62428889a00d4d1baec3cc79056d6a Mon Sep 17 00:00:00 2001 From: kalashshah <202051096@iiitvadodara.ac.in> Date: Thu, 5 Oct 2023 14:28:50 +0530 Subject: [PATCH 10/10] chore: fix styling and add fixed price for nfs --- .../channel/AddSettingModalContent.tsx | 8 +- src/components/channel/ChannelButtons.tsx | 12 +-- src/components/channel/ChannelInfoList.tsx | 7 +- src/components/channel/DepositFeeFooter.tsx | 85 ++++++++----------- .../channel/NotificationSettings.tsx | 33 ++++--- src/config/Themization.js | 8 ++ 6 files changed, 74 insertions(+), 79 deletions(-) diff --git a/src/components/channel/AddSettingModalContent.tsx b/src/components/channel/AddSettingModalContent.tsx index df363676d3..bebae1afd3 100644 --- a/src/components/channel/AddSettingModalContent.tsx +++ b/src/components/channel/AddSettingModalContent.tsx @@ -324,7 +324,7 @@ const ModalContainer = styled.div` } `; -const Label = styled.div` +const Label = styled.div<{ padding?: string }>` font-style: normal; font-weight: 500; font-size: 16px; @@ -347,13 +347,13 @@ const MaxWidthInput = styled(Input)<{ hasError: boolean }>` max-width: 108px; flex: 1; border: ${(props) => - props.hasError ? `1px solid rgba(237, 88, 88, 1)` : `1px solid ${props.theme.default.borderColor}`}; + 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 rgba(237, 88, 88, 1)` : `1px solid ${props.theme.default.borderColor}`}; + props.hasError ? `1px solid ${props.theme.nfsError}` : `1px solid ${props.theme.default.borderColor}`}; `; const ErrorInfo = styled.span` @@ -362,7 +362,7 @@ const ErrorInfo = styled.span` line-height: 18px; letter-spacing: 0em; text-align: left; - color: rgba(237, 88, 88, 1); + color: ${(props) => props.theme.nfsError}; margin-top: 4px; `; diff --git a/src/components/channel/ChannelButtons.tsx b/src/components/channel/ChannelButtons.tsx index 72d6e5d3a8..53b2a2602b 100644 --- a/src/components/channel/ChannelButtons.tsx +++ b/src/components/channel/ChannelButtons.tsx @@ -47,28 +47,23 @@ export const AddSettingButton = ({ onClick }: ChannelButtonProps) => { const ChannelButton = styled(Button)` height: 36px; - background: #cf1c84; + background: ${(props) => props.theme.default.primaryPushThemeTextColor}; color: #fff; z-index: 0; - font-family: 'Strawford'; font-style: normal; font-weight: 500; font-size: 14px; line-height: 17px; border-radius: 8px; padding: 4px 12px 4px 12px; - @media (min-width: 600px) and (max-width: 700px) { - margin-right: 9px; - } `; const ChannelButtonWhite = styled.button` height: 36px; border: 1px solid ${(props) => props.theme.default.borderColor}; background: transparent; - color: #fff; + color: white; z-index: 0; - font-family: 'Strawford'; font-style: normal; font-weight: 500; font-size: 14px; @@ -76,9 +71,6 @@ const ChannelButtonWhite = styled.button` border-radius: 8px; padding: 4px 12px 4px 12px; cursor: pointer; - @media (min-width: 600px) and (max-width: 700px) { - margin-right: 9px; - } `; const ButtonText = styled.span` diff --git a/src/components/channel/ChannelInfoList.tsx b/src/components/channel/ChannelInfoList.tsx index 12a97af4a8..84210415cf 100644 --- a/src/components/channel/ChannelInfoList.tsx +++ b/src/components/channel/ChannelInfoList.tsx @@ -149,14 +149,16 @@ const DelegatesList = styled.div<{ isLoading: boolean }>` const Tag = styled.div` padding: 4px 8px 4px 8px; border-radius: 4px; - background-color: rgba(244, 245, 250, 1); - color: rgba(73, 77, 95, 1); + background-color: ${(props) => props.theme.default.secondaryBg}; + color: ${(props) => props.theme.tooltipContentDesc}; font-size: 10px; margin-left: 8px; `; const NotificationSettingName = styled.span` margin-left: 15px; + color: ${(props) => + props.theme.scheme === 'light' ? props.theme.default.color : props.theme.default.secondaryColor}; `; const EmptyNotificationSetting = styled.div` @@ -169,7 +171,6 @@ const EmptyNotificationSetting = styled.div` `; const EmptyNotificationTitle = styled.div` - font-family: Strawford; font-size: 16px; font-weight: 500; line-height: 24px; diff --git a/src/components/channel/DepositFeeFooter.tsx b/src/components/channel/DepositFeeFooter.tsx index c53141066d..30f49a8480 100644 --- a/src/components/channel/DepositFeeFooter.tsx +++ b/src/components/channel/DepositFeeFooter.tsx @@ -27,30 +27,17 @@ interface DepositFeeFooterProps { onCancel: () => void; disabled: boolean; onClick: () => void; + feeRequired: number; } -const DepositFeeFooter = ({ title, description, onCancel, disabled, onClick }: DepositFeeFooterProps) => { +const DepositFeeFooter = ({ title, description, onCancel, disabled, onClick, feeRequired }: DepositFeeFooterProps) => { const { account, provider } = useAccount(); - const { epnsReadProvider } = useSelector((state: any) => state.contracts); - const [feesRequiredForEdit, setFeesRequiredForEdit] = useState(0); const [pushApprovalAmount, setPushApprovalAmount] = useState(0); const [pushDeposited, setPushDeposited] = useState(false); const [isLoading, setIsLoading] = useState(false); - // it can be fetched from contract for dynamic, but making it const will be fast - const minFees = 50; - const depositFeeToast = useToast(); - useEffect(() => { - if (!account) return; - - (async function () { - const amount = await epnsReadProvider.channelUpdateCounter(account); - setFeesRequiredForEdit(minFees * (Number(amount) + 1)); - })(); - }, [account]); - useEffect(() => { if (!account || !provider) return; @@ -63,7 +50,7 @@ const DepositFeeFooter = ({ title, description, onCancel, disabled, onClick }: D setPushApprovalAmount(parseInt(pushTokenApprovalAmount)); const amountToBeDeposit = parseInt(pushTokenApprovalAmount); - if (amountToBeDeposit >= feesRequiredForEdit && amountToBeDeposit != 0) { + if (amountToBeDeposit >= feeRequired && amountToBeDeposit != 0) { setPushDeposited(true); } else { setPushDeposited(false); @@ -80,12 +67,12 @@ const DepositFeeFooter = ({ title, description, onCancel, disabled, onClick }: D const response = await approvePushToken({ signer, contractAddress: addresses.epnscore, - amount: feesRequiredForEdit - pushApprovalAmount, + amount: feeRequired - pushApprovalAmount, }); console.log('response', response); if (response) { setIsLoading(false); - setPushApprovalAmount(feesRequiredForEdit); + setPushApprovalAmount(feeRequired); setPushDeposited(true); depositFeeToast.showMessageToast({ toastTitle: 'Success', @@ -143,11 +130,11 @@ const DepositFeeFooter = ({ title, description, onCancel, disabled, onClick }: D

{pushDeposited ? : null} - {feesRequiredForEdit} PUSH + {feeRequired} PUSH { await mintPushToken({ noOfTokens, provider, account }); @@ -170,18 +157,27 @@ const DepositFeeFooter = ({ title, description, onCancel, disabled, onClick }: D <> {/* This below is Footer Buttons i.e, Cancel and save changes */} - Cancel - - {pushApprovalAmount >= feesRequiredForEdit ? ( + + Cancel + + + {pushApprovalAmount >= feeRequired ? ( Save Changes ) : ( - Approve PUSH + + Approve PUSH + )} @@ -220,7 +216,6 @@ const Footer = styled(ItemVV2)` const FooterPrimaryText = styled.p` margin: 0px; color: ${(props) => props.theme.editChannelPrimaryText}; - font-family: 'Strawford'; font-style: normal; font-weight: 500; font-size: 20px; @@ -237,8 +232,7 @@ const FooterSecondaryText = styled.p` const EditFee = styled.p` margin: 0px 0px 0px 5px; - color: #d53893; - font-family: 'Strawford'; + color: ${(props) => props.theme.viewChannelSecondaryText}; font-style: normal; font-weight: 500; font-size: 20px; @@ -255,7 +249,6 @@ const VerifyingContainer = styled(ItemVV2)` `; const TransactionText = styled.p` - font-family: 'Strawford'; font-style: normal; font-weight: 500; font-size: 18px; @@ -275,8 +268,7 @@ const ButtonContainer = styled(ItemHV2)` } `; -const FooterButtons = styled(Button)` - font-family: 'Strawford'; +const FooterButtons = styled(Button)<{ disabled: boolean }>` font-style: normal; font-weight: 500; font-size: 18px; @@ -285,8 +277,8 @@ const FooterButtons = styled(Button)` border-radius: 15px; align-items: center; text-align: center; - background: #cf1c84; - color: #fff; + background: ${(props) => (props.disabled ? props.theme.nfsDisabled : props.theme.default.primaryPushThemeTextColor)}; + color: ${(props) => (props.disabled ? props.theme.nfsDisabledText : 'white')}; padding: 16px 27px; width: 12rem; @@ -302,23 +294,16 @@ const FooterButtons = styled(Button)` `; const CancelButtons = styled(FooterButtons)` - margin-right:14px; - background:${(props) => props.theme.default.bg}; - color:${(props) => props.theme.logoBtnColor}; - border:1px solid #CF1C84; - @media (max-width:425px){ - margin-right:0px; - margin-top:10px; - } + 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}; - &:hover{ - color:#AC106C; - border:border: 1px solid #AC106C; - background:transparent; - opacity:1; - } - - &:after{ - background:white; + @media ${device.mobileL} { + margin-right: 0px; + margin-top: 10px; } `; diff --git a/src/components/channel/NotificationSettings.tsx b/src/components/channel/NotificationSettings.tsx index 30026ce5d0..cd68c06845 100644 --- a/src/components/channel/NotificationSettings.tsx +++ b/src/components/channel/NotificationSettings.tsx @@ -24,21 +24,18 @@ import { useAccount } from 'hooks'; import { appConfig } from 'config'; import useModalBlur, { MODAL_POSITION } from 'hooks/useModalBlur'; import { ChannelSetting } from 'helpers/channel/types'; +import { getChannel } from 'services'; // Constants const CORE_CHAIN_ID = appConfig.coreContractChain; function NotificationSettings() { const { account, chainId } = useAccount(); - const { - coreChannelAdmin, - channelDetails, - delegatees, - aliasDetails: { aliasEthAddr }, - } = useSelector((state: any) => state.admin); + const { coreChannelAdmin, delegatees } = useSelector((state: any) => state.admin); const { epnsWriteProvider } = useSelector((state: any) => state.contracts); const onCoreNetwork = CORE_CHAIN_ID === chainId; + const EDIT_SETTING_FEE = 50; const [channelAddress, setChannelAddress] = React.useState(''); const [settings, setSettings] = React.useState([]); @@ -53,14 +50,25 @@ function NotificationSettings() { 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 - setIsLoading(true); - if (coreChannelAdmin && account && coreChannelAdmin !== account) { - const url = window.location.origin; - window.location.replace(`${url}/channels`); - } - setIsLoading(false); + (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(() => { @@ -257,6 +265,7 @@ function NotificationSettings() { ]} />