From 46103d4087807954cbd865e6e14a06131885bb1c Mon Sep 17 00:00:00 2001 From: RXRD <118821868+RiXelanya@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:57:15 +0700 Subject: [PATCH] fix: add exclusive timeline feature (#1959) * fix: add exclusive timeline * fix: add subscription button for exclusive timeline * format fix * Update ExperienceCard.tsx --- .../BasicExperienceEditor.tsx | 18 +++++- .../Timeline/Render/ExperienceCard.tsx | 63 ++++++++++++++++++- src/interfaces/experience.ts | 1 + src/interfaces/interaction.ts | 1 + src/lib/api/post.ts | 21 ++++--- src/lib/services/polkadot-js.ts | 43 ++----------- src/locale/en.json | 1 + src/locale/fra.json | 1 + src/locale/id.json | 1 + src/locale/ru.json | 1 + 10 files changed, 99 insertions(+), 52 deletions(-) diff --git a/src/components/ExperienceEditor/BasicExperienceEditor.tsx b/src/components/ExperienceEditor/BasicExperienceEditor.tsx index e3255ae95..2ad4c9622 100644 --- a/src/components/ExperienceEditor/BasicExperienceEditor.tsx +++ b/src/components/ExperienceEditor/BasicExperienceEditor.tsx @@ -10,11 +10,14 @@ import React, { useState, useRef } from 'react'; import { Button, FormControl, + FormControlLabel, + FormGroup, FormHelperText, IconButton, InputLabel, OutlinedInput, SvgIcon, + Switch, TextField, Typography, } from '@material-ui/core'; @@ -96,11 +99,19 @@ export const BasicExperienceEditor: React.FC = selectedUserId: false, }); + const onSwitch = (event: React.ChangeEvent) => { + const value = event.target.checked; + onExperience(prevExperience => ({ + ...prevExperience, + exclusive: value, + })); + setDetailChanged(experience['exclusive'] !== value); + }; + const handleChange = (field: keyof ExperienceProps) => (event: React.ChangeEvent) => { const value = event.target.value.trimStart(); - onExperience(prevExperience => ({ ...prevExperience, [field]: value, @@ -242,6 +253,11 @@ export const BasicExperienceEditor: React.FC = + + } + label="Exclusive"> +
diff --git a/src/components/Timeline/Render/ExperienceCard.tsx b/src/components/Timeline/Render/ExperienceCard.tsx index 799b9aabe..106e2760d 100644 --- a/src/components/Timeline/Render/ExperienceCard.tsx +++ b/src/components/Timeline/Render/ExperienceCard.tsx @@ -2,6 +2,7 @@ import { DotsVerticalIcon, DuplicateIcon } from '@heroicons/react/outline'; import React, { useState } from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; +import { useSelector } from 'react-redux'; import getConfig from 'next/config'; import dynamic from 'next/dynamic'; @@ -21,6 +22,8 @@ import { import Menu from '@material-ui/core/Menu'; import BaseMenuItem from '@material-ui/core/MenuItem'; +import { BN, BN_TEN } from '@polkadot/util'; + import { useStyles } from './experience-card.style'; import { WithAuthorizeAction } from 'components/common/Authorization/WithAuthorizeAction'; @@ -29,9 +32,14 @@ import ShowIf from 'components/common/show-if.component'; import { Modal } from 'src/components/atoms/Modal'; import useConfirm from 'src/components/common/Confirm/use-confirm.hook'; import { useExperienceHook } from 'src/hooks/use-experience-hook'; +import { useWallet } from 'src/hooks/use-wallet-hook'; import { Experience, WrappedExperience } from 'src/interfaces/experience'; +import { ReferenceType } from 'src/interfaces/interaction'; import { User } from 'src/interfaces/user'; +import * as UserAPI from 'src/lib/api/user'; import i18n from 'src/locale'; +import { RootState } from 'src/reducers'; +import { BalanceState } from 'src/reducers/balance/reducer'; const MenuItem = WithAuthorizeAction(BaseMenuItem); @@ -59,12 +67,14 @@ export const ExperienceCard: React.FC = props => { const confirm = useConfirm(); const enqueueSnackbar = useEnqueueSnackbar(); const [promptSignin, setPromptSignin] = useState(false); + const [isSubscribing, setSubscribing] = useState(false); const [menuAnchorElement, setMenuAnchorElement] = useState(null); const [shareAnchorElement, setShareAnchorElement] = useState(null); const { userExperiencesMeta, removeExperience, loadExperience } = useExperienceHook(); + const { sendTip } = useWallet(); const link = publicRuntimeConfig.appAuthURL + `?type=all&id=${experience.id}`; const isOwnExperience = experience?.createdBy === user?.id; const isHidden = () => { @@ -72,6 +82,11 @@ export const ExperienceCard: React.FC = props => { if (experience.private && experience.friend) return false; return false; }; + const isExclusive = experience.exclusive ? experience.exclusive : false; + + const { balanceDetails: balances } = useSelector( + state => state.balanceState, + ); const isSubscribed = () => { return ( @@ -175,6 +190,29 @@ export const ExperienceCard: React.FC = props => { }); }; + const handleSubscription = () => { + setSubscribing(true); + }; + + const closeSubscribing = () => { + setSubscribing(false); + }; + + const handlePaySubscription = async () => { + const receiver = await UserAPI.getWalletAddress(experience.createdBy); + const defaultCurrency = balances[0]; + const amount = BN_TEN.pow(new BN(defaultCurrency.decimal - 2)); + await sendTip( + receiver, + amount, + balances[0], + ReferenceType.EXCLUSIVE_TIMELINE, + experience.id, + ); + handleSubscribeExperience(); + closeSubscribing(); + }; + const confirmDeleteExperience = () => { handleCloseSettings(); @@ -258,12 +296,19 @@ export const ExperienceCard: React.FC = props => { variant="contained" color="primary" size="small" + disabled={isExclusive && isSubscribed()} onClick={ - isSubscribed() + isExclusive + ? handleSubscription + : isSubscribed() ? openUnsubscribeConfirmation : handleSubscribeExperience }> - {isSubscribed() + {isExclusive + ? isSubscribed() + ? 'subscribed' + : i18n.t('Experience.Preview.Button.Subscription') + : isSubscribed() ? i18n.t('Experience.Preview.Button.Unsubscribe') : i18n.t('Experience.Preview.Button.Subscribe')} @@ -382,6 +427,20 @@ export const ExperienceCard: React.FC = props => { />
+ + + setPromptSignin(false)} diff --git a/src/interfaces/experience.ts b/src/interfaces/experience.ts index ab76afd99..b72e4b6f0 100644 --- a/src/interfaces/experience.ts +++ b/src/interfaces/experience.ts @@ -44,6 +44,7 @@ export interface ExperienceProps extends Searchable { visibility: string; selectedUserIds: SelectedUserIds[]; editorsId?: string[]; + exclusive?: boolean; } export interface Experience extends ExperienceProps, BaseModel { diff --git a/src/interfaces/interaction.ts b/src/interfaces/interaction.ts index 10942559c..085b74d96 100644 --- a/src/interfaces/interaction.ts +++ b/src/interfaces/interaction.ts @@ -6,6 +6,7 @@ export enum ReferenceType { USER = 'user', PEOPLE = 'people', EXCLUSIVE_CONTENT = 'unlockable_content', + EXCLUSIVE_TIMELINE = 'unlockable_timeline', } export enum SectionType { diff --git a/src/lib/api/post.ts b/src/lib/api/post.ts index 227f6e0b7..188a9777c 100644 --- a/src/lib/api/post.ts +++ b/src/lib/api/post.ts @@ -133,6 +133,17 @@ export const getPost = async ( params.owner = userId; } + if (params.experienceId) { + const { data } = await MyriadAPI().request({ + url: `/experience/${params.experienceId}/posts`, + method: 'GET', + params: { + filter: filterParams, + }, + }); + return data; + } + const { data } = await MyriadAPI().request({ url: '/user/posts', method: 'GET', @@ -156,16 +167,6 @@ export const getPost = async ( return post.createdBy === fields.owner; }); } - if (data.data.length === 0 && params.experienceId) { - const { data } = await MyriadAPI().request({ - url: `/experience/${params.experienceId}/posts`, - method: 'GET', - params: { - filter: filterParams, - }, - }); - return data; - } return data; }; diff --git a/src/lib/services/polkadot-js.ts b/src/lib/services/polkadot-js.ts index e2b4a0d9c..26f40890d 100644 --- a/src/lib/services/polkadot-js.ts +++ b/src/lib/services/polkadot-js.ts @@ -260,62 +260,27 @@ export class PolkadotJs implements IProvider { callback && callback({ signerOpened: true }); - // here we use the api to create a balance transfer to some account of a value of 12345678 const { referenceId: accountId, referenceType } = walletDetail; const isWalletAddress = referenceType === WalletReferenceType.WALLET_ADDRESS; const assetId = parseInt(referenceId); const transferExtrinsic = isWalletAddress ? !referenceId - ? api.tx.balances.transfer(accountId, amount) + ? api.tx.balances.transferKeepAlive(accountId, amount) : api.tx.octopusAssets.transfer(assetId, accountId, amount) : api.tx.tipping.sendTip(walletDetail, amount); // passing the injected account address as the first argument of signAndSend // will allow the api to retrieve the signer and the user will see the extension // popup asking to sign the balance transfer transaction - const txInfo = await transferExtrinsic.signAsync(signer.address, { + + const txInfo = await transferExtrinsic.signAndSend(signer.address, { signer: injector.signer, // make sure nonce does not stuck nonce: -1, }); - const txHash: string = await new Promise((resolve, reject) => { - txInfo - .send(({ status, isError, dispatchError }) => { - if (status.isInBlock) { - console.log(`\tBlock hash : ${status.asInBlock.toHex()}`); - } else if (status.isFinalized) { - console.log(`\tFinalized : ${status.asFinalized.toHex()}`); - resolve(status.asFinalized.toHex()); - } else if (isError) { - console.log(`\tFinalized : null`); - reject('FailedToSendTip'); - } - - if (dispatchError) { - if (dispatchError.isModule) { - const { name } = api.registry.findMetaError( - dispatchError.asModule, - ); - - reject(new Error(name)); - } else { - const dispatchErrorType = dispatchError.toString(); - const parseDispatch = JSON.parse(dispatchErrorType); - - const values: string[] = Object.values(parseDispatch); - - reject(new Error(values[0] ?? 'ExtrinsicFailed')); - } - } - }) - .catch(err => { - reject(err); - }); - }); - - return txHash; + return txInfo.toHex(); } catch (error) { console.log(error); throw error; diff --git a/src/locale/en.json b/src/locale/en.json index 5bb221e3b..b49dd5253 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -64,6 +64,7 @@ "Edit": "Edit timeline", "Subscribe": "Follow", "Unsubscribe": "Unfollow", + "Subscription": "Subscribe", "Clone": "Clone" }, "Subheader": { diff --git a/src/locale/fra.json b/src/locale/fra.json index e7629ef71..915cba823 100644 --- a/src/locale/fra.json +++ b/src/locale/fra.json @@ -64,6 +64,7 @@ "Edit": "Edit timeline", "Subscribe": "Follow", "Unsubscribe": "Unfollow", + "Subscription": "Subscribe", "Clone": "Cloner" }, "Subheader": { diff --git a/src/locale/id.json b/src/locale/id.json index f0f8c526b..f676dccf0 100644 --- a/src/locale/id.json +++ b/src/locale/id.json @@ -64,6 +64,7 @@ "Edit": "Ubah Timeline", "Subscribe": "Mengikuti", "Unsubscribe": "Berhenti mengikuti", + "Subscription": "Subscribe", "Clone": "Klon" }, "Subheader": { diff --git a/src/locale/ru.json b/src/locale/ru.json index 15a84140d..7c8cf2457 100644 --- a/src/locale/ru.json +++ b/src/locale/ru.json @@ -59,6 +59,7 @@ "Edit": "Редактировать опыт", "Subscribe": "Следовать", "Unsubscribe": "Отписаться", + "Subscription": "Subscribe", "Clone": "Копия" }, "Subheader": {