diff --git a/src/components/pages/Roles/RolesTable.tsx b/src/components/pages/Roles/RolesTable.tsx index d73f535e18..6a6b8a60c0 100644 --- a/src/components/pages/Roles/RolesTable.tsx +++ b/src/components/pages/Roles/RolesTable.tsx @@ -221,7 +221,7 @@ export function RolesRowEdit({ editStatus={editStatus} /> - + p.isStreaming()).length || undefined} /> ); } @@ -265,7 +265,7 @@ export function RolesTable({ paymentsCount={ role.payments === undefined ? undefined - : role.payments.filter(p => p.isStreaming()).length + : role.payments.filter(p => p.isStreaming()).length || undefined } /> ))} diff --git a/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx b/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx index 7e03a8fa0c..797ea29a16 100644 --- a/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx +++ b/src/components/pages/Roles/forms/RoleFormPaymentStreams.tsx @@ -41,6 +41,7 @@ export function RoleFormPaymentStreams() { asset: payment.asset, endDate: payment.endDate, startDate: payment.startDate, + cliffDate: payment.cliffDate, isStreaming: () => false, }} onClick={() => { diff --git a/src/components/pages/Roles/forms/RoleFormTabs.tsx b/src/components/pages/Roles/forms/RoleFormTabs.tsx index a2d60d148b..d037cbfddb 100644 --- a/src/components/pages/Roles/forms/RoleFormTabs.tsx +++ b/src/components/pages/Roles/forms/RoleFormTabs.tsx @@ -1,13 +1,15 @@ -import { Tab, TabList, TabPanels, TabPanel, Tabs, Button, Flex } from '@chakra-ui/react'; +import { Button, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react'; import { useFormikContext } from 'formik'; import { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { Hex } from 'viem'; +import { TOOLTIP_MAXW } from '../../../../constants/common'; import { DAO_ROUTES } from '../../../../constants/routes'; import { useFractal } from '../../../../providers/App/AppProvider'; import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; import { useRolesStore } from '../../../../store/roles'; +import ModalTooltip from '../../../ui/modals/ModalTooltip'; import { EditBadgeStatus, RoleFormValues, RoleHatFormValue } from '../types'; import RoleFormInfo from './RoleFormInfo'; import RoleFormPaymentStream from './RoleFormPaymentStream'; @@ -30,6 +32,7 @@ export default function RoleFormTabs({ const { editedRoleData, isRoleUpdated, existingRoleHat } = useRoleFormEditedRole({ hatsTree }); const { t } = useTranslation(['roles']); const { values, errors, setFieldValue, setTouched } = useFormikContext(); + const paymentsTooltipRef = useRef(null); useEffect(() => { if (values.hats.length && !values.roleEditing) { @@ -64,15 +67,34 @@ export default function RoleFormTabs({ {t('roleInfo')} - {t('payments')} + + {!hatsTree ? ( + + {t('payments')} + + ) : ( + t('payments') + )} + - - - + {!!hatsTree && ( + + + + )} { - if (fieldNames.includes(fieldName) && isRemoved) { +const addRemoveField = (fieldNames: string[], fieldName: string, hasChanges: boolean) => { + if (fieldNames.includes(fieldName) && !hasChanges) { return fieldNames.filter(field => field !== fieldName); + } else if (!fieldNames.includes(fieldName) && !hasChanges) { + return fieldNames; } return [...fieldNames, fieldName]; }; diff --git a/src/hooks/DAO/loaders/useHatsTree.ts b/src/hooks/DAO/loaders/useHatsTree.ts index 943cd7cbf9..22a24250ef 100644 --- a/src/hooks/DAO/loaders/useHatsTree.ts +++ b/src/hooks/DAO/loaders/useHatsTree.ts @@ -1,5 +1,5 @@ import { useApolloClient } from '@apollo/client'; -import { Tree, HatsSubgraphClient } from '@hatsprotocol/sdk-v1-subgraph'; +import { HatsSubgraphClient, Tree } from '@hatsprotocol/sdk-v1-subgraph'; import { useEffect } from 'react'; import { toast } from 'react-toastify'; import { formatUnits, getAddress } from 'viem'; @@ -8,7 +8,7 @@ import { StreamsQueryDocument } from '../../../../.graphclient'; import { SablierPayment } from '../../../components/pages/Roles/types'; import useIPFSClient from '../../../providers/App/hooks/useIPFSClient'; import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; -import { useRolesStore, DecentHatsError } from '../../../store/roles'; +import { DecentHatsError, useRolesStore } from '../../../store/roles'; import { CacheExpiry, CacheKeys } from '../../utils/cache/cacheDefaults'; import { getValue, setValue } from '../../utils/cache/useLocalStorage'; @@ -210,19 +210,16 @@ const useHatsTree = () => { endDate, cliffDate, isStreaming: () => { - const start = - lockupLinearStream.cliff === undefined && - lockupLinearStream.startTime !== undefined - ? startDate.getTime() - : cliffDate !== undefined - ? cliffDate.getTime() - : undefined; + const start = !lockupLinearStream.cliff + ? startDate.getTime() + : cliffDate !== undefined + ? cliffDate.getTime() + : undefined; const end = endDate ? endDate.getTime() : undefined; const cancelled = lockupLinearStream.canceled; const now = new Date().getTime(); - const isStreaming = - !cancelled && !!start && !!end && start <= now && end > now; - return isStreaming; + + return !cancelled && !!start && !!end && start <= now && end > now; }, }; }, diff --git a/src/hooks/streams/useCreateSablierStream.ts b/src/hooks/streams/useCreateSablierStream.ts index e4d8d51d97..0af300e021 100644 --- a/src/hooks/streams/useCreateSablierStream.ts +++ b/src/hooks/streams/useCreateSablierStream.ts @@ -56,23 +56,20 @@ export default function useCreateSablierStream() { throw new Error('Start date of the stream can not be larger than end date'); } - let cliffDuration = 0; if (cliffDateTs) { if (cliffDateTs <= startDateTs) { throw new Error('Cliff date can not be less or equal than start date'); } else if (cliffDateTs >= endDateTs) { throw new Error('Cliff date can not be larger or equal than end date'); } - cliffDuration = Math.ceil((cliffDateTs - startDateTs) / 1000); } - const streamDuration = Math.ceil((endDateTs - startDateTs) / 1000); - return { ...prepareBasicStreamData(recipient, totalAmount), - durations: { - cliff: cliffDuration, - total: streamDuration + cliffDuration, // Total duration has to include cliff duration + timestamps: { + start: startDateTs, + end: endDateTs, + cliff: cliffDateTs, }, }; }, @@ -126,7 +123,7 @@ export default function useCreateSablierStream() { preparedStreamCreationTransactions.push({ calldata: encodeFunctionData({ abi: SablierV2BatchAbi, - functionName: 'createWithDurationsLL', // @dev @todo Another option would be to use `createWithTimestampsLL`. Probably makes sense to change the logic to `createWithTimestampsLL` since we drifted away from "durations" and actually operating with timestamps always + functionName: 'createWithTimestampsLL', args: [sablierV2LockupLinear, tokenAddress, assembledStreams], }), targetAddress: sablierV2Batch, diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index e44da0bd4f..c938755a2c 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -174,7 +174,7 @@ const identifyAndPrepareEditedPaymentStreams = ( return modifiedHats.flatMap(formHat => { const currentHat = getHat(formHat.id); if (currentHat === null) { - throw new Error("Couldn't find existing Hat for edited payment stream Hat."); + return []; } if (formHat.payments === undefined) { @@ -211,9 +211,9 @@ const identifyAndPrepareEditedPaymentStreams = ( return { streamId: payment.streamId, recipient: currentHat.smartAddress, - startDateTs: payment.startDate.getTime(), - endDateTs: payment.endDate.getTime(), - cliffDateTs: payment.cliffDate?.getTime() ?? 0, + startDateTs: Math.floor(payment.startDate.getTime() / 1000), + endDateTs: Math.ceil(payment.endDate.getTime() / 1000), + cliffDateTs: Math.floor((payment.cliffDate?.getTime() ?? 0) / 1000), totalAmount: payment.amount.bigintValue, assetAddress: payment.asset.address, roleHatId: BigInt(currentHat.id), @@ -232,7 +232,7 @@ const identifyAndPrepareAddedPaymentStreams = async ( ): Promise => { const preparedStreamDataMapped = await Promise.all( modifiedHats.map(async formHat => { - if (formHat.payments === undefined || formHat.editedRole.status !== EditBadgeStatus.Updated) { + if (formHat.payments === undefined) { return []; } @@ -263,9 +263,9 @@ const identifyAndPrepareAddedPaymentStreams = async ( return { recipient: recipientAddress, - startDateTs: payment.startDate.getTime(), - endDateTs: payment.endDate.getTime(), - cliffDateTs: payment.cliffDate?.getTime() ?? 0, + startDateTs: Math.floor(payment.startDate.getTime() / 1000), + endDateTs: Math.ceil(payment.endDate.getTime() / 1000), + cliffDateTs: Math.floor((payment.cliffDate?.getTime() ?? 0) / 1000), totalAmount: payment.amount.bigintValue, assetAddress: payment.asset.address, }; @@ -663,14 +663,8 @@ export default function useCreateRoles() { if (addedPaymentStreams.length) { const preparedPaymentTransactions = prepareBatchLinearStreamCreation(addedPaymentStreams); - addedPaymentStreams.forEach((_, i) => { - hatPaymentAddedTxs.push( - preparedPaymentTransactions.preparedTokenApprovalsTransactions[i], - ); - hatPaymentAddedTxs.push( - preparedPaymentTransactions.preparedStreamCreationTransactions[i], - ); - }); + hatPaymentAddedTxs.push(...preparedPaymentTransactions.preparedTokenApprovalsTransactions); + hatPaymentAddedTxs.push(...preparedPaymentTransactions.preparedStreamCreationTransactions); } if (editedPaymentStreams.length) { @@ -733,14 +727,8 @@ export default function useCreateRoles() { const preparedPaymentTransactions = prepareBatchLinearStreamCreation(editedPaymentStreams); hatPaymentEditedTxs.push(...paymentCancelTxs); - editedPaymentStreams.forEach((_, i) => { - hatPaymentEditedTxs.push( - preparedPaymentTransactions.preparedTokenApprovalsTransactions[i], - ); - hatPaymentEditedTxs.push( - preparedPaymentTransactions.preparedStreamCreationTransactions[i], - ); - }); + hatPaymentEditedTxs.push(...preparedPaymentTransactions.preparedTokenApprovalsTransactions); + hatPaymentEditedTxs.push(...preparedPaymentTransactions.preparedStreamCreationTransactions); } const proposalTransactions = { diff --git a/src/i18n/locales/en/roles.json b/src/i18n/locales/en/roles.json index 846fd01b38..fdee444b58 100644 --- a/src/i18n/locales/en/roles.json +++ b/src/i18n/locales/en/roles.json @@ -56,6 +56,7 @@ "activePayments": "Active Payments", "payments": "Payments", "payment": "Payment", + "tipPaymentsDisabled": "Creating payments can be accessed after initializing roles. You need to create at least 1 role in order to create payments for it or for other roles.", "withdraw": "Withdraw", "available": "Available", "withdrawPendingMessage": "Withdrawing your payment, hang tight", diff --git a/src/pages/daos/[daoAddress]/roles/index.tsx b/src/pages/daos/[daoAddress]/roles/index.tsx index 72c54ea5ca..32028de4ed 100644 --- a/src/pages/daos/[daoAddress]/roles/index.tsx +++ b/src/pages/daos/[daoAddress]/roles/index.tsx @@ -83,7 +83,7 @@ function Roles() { wearerAddress={roleHat.wearer || zeroAddress} hatId={roleHat.id} handleRoleClick={handleNavigateToRole} - paymentsCount={roleHat.payments?.length} + paymentsCount={roleHat.payments?.filter(p => p.isStreaming()).length || undefined} /> ))}