Skip to content

Commit

Permalink
feat: add superlike component
Browse files Browse the repository at this point in the history
  • Loading branch information
teodorus-nathaniel committed Dec 29, 2023
1 parent a479772 commit a2e767a
Show file tree
Hide file tree
Showing 11 changed files with 814 additions and 29 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"@polkadot/ui-keyring": "^2.9.8",
"@reduxjs/toolkit": "^1.3.4",
"@subsocial/api": "0.8.10",
"@subsocial/data-hub-sdk": "dappforce/subsocial-data-hub-sdk#staging",
"@subsocial/definitions": "0.8.10",
"@subsocial/elasticsearch": "0.8.10",
"@subsocial/grill-widget": "^0.0.13",
Expand Down Expand Up @@ -160,6 +161,7 @@
"store": "^2.0.12",
"strip-markdown": "^4.0.0",
"url-loader": "^4.1.1",
"yup": "^0.32.9"
"yup": "^0.32.9",
"zod": "^3.22.4"
}
}
4 changes: 2 additions & 2 deletions src/components/posts/view-post/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import NoData from 'src/components/utils/EmptyList'
import { EntityStatusProps, HiddenEntityPanel } from 'src/components/utils/EntityStatusPanels'
import Segment from 'src/components/utils/Segment'
import { ActiveVoters, PostVoters } from 'src/components/voting/ListVoters'
import { VoterButtons } from 'src/components/voting/VoterButtons'
import SuperLike from 'src/components/voting/SuperLike'
import { maintenanceMsg } from 'src/config/env'
import { resolveIpfsUrl } from 'src/ipfs'
import messages from 'src/messages'
Expand Down Expand Up @@ -293,7 +293,7 @@ export const PostActionsPanel: FC<PostActionsPanelProps> = props => {
post: { struct },
} = postDetails

const ReactionsAction = () => <VoterButtons post={struct} className='DfAction' />
const ReactionsAction = () => <SuperLike post={struct} className='DfAction' />

return (
<div className={`DfActionsPanel ${withBorder && 'DfActionBorder'} ${className ?? ''}`}>
Expand Down
15 changes: 15 additions & 0 deletions src/components/utils/datahub-queue/super-likes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SocialCallDataArgs, socialCallName } from '@subsocial/data-hub-sdk'
import axios from 'axios'
import { createSocialDataEventPayload, DatahubParams } from './utils'

export async function createSuperLike(
params: DatahubParams<SocialCallDataArgs<'synth_active_staking_create_super_like'>>,
) {
const input = createSocialDataEventPayload(
socialCallName.synth_active_staking_create_super_like,
params,
)

const res = await axios.post('/api/datahub/super-likes', input)
return res.data
}
36 changes: 36 additions & 0 deletions src/components/utils/datahub-queue/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
SocialCallDataArgs,
socialCallName,
SocialEventDataApiInput,
SocialEventDataType,
socialEventProtVersion,
} from '@subsocial/data-hub-sdk'

export type DatahubParams<T> = {
address: string

uuid?: string
timestamp?: number

args: T
}

export function createSocialDataEventPayload<T extends keyof typeof socialCallName>(
callName: T,
{ timestamp, address, uuid, args }: DatahubParams<SocialCallDataArgs<T>>,
) {
const payload: SocialEventDataApiInput = {
protVersion: socialEventProtVersion['0.1'],
dataType: SocialEventDataType.offChain,
callData: {
name: callName,
signer: address || '',
args: JSON.stringify(args),
timestamp: timestamp || Date.now(),
uuid: uuid || crypto.randomUUID(),
},
providerAddr: address,
sig: '',
}
return payload
}
56 changes: 56 additions & 0 deletions src/components/voting/SuperLike.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { PostStruct } from '@subsocial/api/types'
import { Button, ButtonProps } from 'antd'
import clsx from 'clsx'
import { CSSProperties } from 'react'
import { AiFillHeart, AiOutlineHeart } from 'react-icons/ai'
import { useOpenCloseOnBoardingModal } from 'src/rtk/features/onBoarding/onBoardingHooks'
import { useAuth } from '../auth/AuthContext'
import { useMyAddress } from '../auth/MyAccountsContext'
import { IconWithLabel } from '../utils'
import { createSuperLike } from '../utils/datahub-queue/super-likes'

export type SuperLikeProps = ButtonProps & {
post: PostStruct
}

export default function SuperLike({ post, ...props }: SuperLikeProps) {
const isActive = true
const count = 21
const openOnBoardingModal = useOpenCloseOnBoardingModal()
const myAddress = useMyAddress()

const {
openSignInModal,
state: {
completedSteps: { hasTokens },
},
} = useAuth()

const onClick = async () => {
if (!myAddress) {
openSignInModal()
return
}
if (!hasTokens) {
openOnBoardingModal('open', { type: 'partial', toStep: 'energy' })
return
}

await createSuperLike({ address: myAddress, args: { postId: post.id } })
}

const likeStyle: CSSProperties = { position: 'relative', top: '0.07em' }
const icon = isActive ? (
<AiFillHeart className='FontSemilarge ColorPrimary' style={likeStyle} />
) : (
<AiOutlineHeart className='FontSemilarge' style={likeStyle} />
)
return (
<Button
className={clsx('DfVoterButton ColorMuted', isActive && 'ColorPrimary', props.className)}
onClick={onClick}
>
<IconWithLabel icon={icon} count={count} />
</Button>
)
}
6 changes: 6 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ export const appOverrides: Partial<AppConfig> = {

export const ampId = process.env['AMP_ID'] || ''

export const serverMnemonic = process.env['SERVER_MNEMONIC']
export const datahubQueueConfig = {
url: process.env['DATAHUB_QUEUE_URL'],
token: process.env['DATAHUB_QUEUE_TOKEN'],
}

/**
* Enable or disable the available features of this web app by overriding them in the .env file.
*/
Expand Down
31 changes: 31 additions & 0 deletions src/pages/api/datahub/super-likes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SocialEventDataApiInput } from '@subsocial/data-hub-sdk'
import { NextApiRequest, NextApiResponse } from 'next'
import { ApiResponse, handlerWrapper } from 'src/server/common'
import { createSuperLikeServer } from 'src/server/datahub-queue/super-likes'
import { z } from 'zod'

export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
return POST_handler(req, res)
}

return res.status(405).send('Method Not Allowed')
}

export type ApiDatahubModerationBody = SocialEventDataApiInput
export type ApiDatahubModerationResponse = ApiResponse
const POST_handler = handlerWrapper({
inputSchema: z.any(),
dataGetter: req => req.body,
})({
allowedMethods: ['POST'],
errorLabel: 'super-likes',
handler: async (data: ApiDatahubModerationBody, _req, res) => {
await createSuperLikeServer(data)

res.json({
message: 'OK',
success: true,
})
},
})
86 changes: 86 additions & 0 deletions src/server/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Keyring } from '@polkadot/keyring'
import { waitReady } from '@polkadot/wasm-crypto'
import { NextApiRequest, NextApiResponse } from 'next'
import { serverMnemonic } from 'src/config/env'
import { z } from 'zod'

export type ApiResponse<T = {}> = T & {
success: boolean
message: string
errors?: any
}

export function handlerWrapper<Input extends z.ZodTypeAny>(config: {
inputSchema: Input | null
dataGetter: (req: NextApiRequest) => unknown
}) {
return <Output>(handlerConfig: {
errorLabel?: string
handler: (
data: z.infer<Input>,
req: NextApiRequest,
res: NextApiResponse<ApiResponse<Output>>,
) => PromiseLike<void>
allowedMethods?: ('GET' | 'POST' | 'PUT' | 'DELETE')[]
}) => {
return async (req: NextApiRequest, res: NextApiResponse<ApiResponse<Output>>) => {
const { handler, allowedMethods, errorLabel } = handlerConfig

if (!allowedMethods?.includes(req.method ?? ('' as any))) return res.status(404).end()

const { dataGetter, inputSchema } = config
if (!inputSchema) {
try {
return await handler(null, req, res)
} catch (err) {
console.error(`Error in ${errorLabel || 'handler'}:`, err)
return res.status(500).send({
success: false,
message: 'Internal server error',
errors: err,
} as ApiResponse<Output>)
}
}

const params = inputSchema.safeParse(dataGetter(req))

if (!params.success) {
return res.status(400).send({
success: false,
message: 'Invalid request body',
errors: params.error.errors,
} as ApiResponse<Output>)
}

try {
return await handler(params.data, req, res)
} catch (err) {
console.error(`Error in ${errorLabel || 'handler'}:`, err)
return res.status(500).send({
success: false,
message: 'Internal server error',
errors: err,
} as ApiResponse<Output>)
}
}
}
}

export function getCommonErrorMessage(e: any, fallbackMsg = 'Error has been occurred') {
return e ? { message: fallbackMsg, ...e }.message : fallbackMsg
}

export function convertNonce(nonce: number) {
const newNonce = new Uint8Array(24)

newNonce[0] = nonce

return newNonce
}

export async function getServerAccount() {
if (!serverMnemonic) throw new Error('Server Mnemonic is not set')
const keyring = new Keyring()
await waitReady()
return keyring.addFromMnemonic(serverMnemonic, {}, 'sr25519')
}
22 changes: 22 additions & 0 deletions src/server/datahub-queue/super-likes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { SocialEventDataApiInput } from '@subsocial/data-hub-sdk'
import { gql } from 'graphql-request'
import { backendSigWrapper, datahubQueueRequest } from './utils'

const CREATE_SUPER_LIKE = gql`
mutation CreateSuperLike($createSuperLikeInput: CreateMutateActiveStakingSuperLikeInput!) {
activeStakingCreateSuperLike(args: $createSuperLikeInput) {
processed
message
}
}
`

export async function createSuperLikeServer(input: SocialEventDataApiInput) {
const signedPayload = await backendSigWrapper(input)
await datahubQueueRequest({
document: CREATE_SUPER_LIKE,
variables: {
createSuperLikeInput: signedPayload,
},
})
}
49 changes: 49 additions & 0 deletions src/server/datahub-queue/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { KeyringPair } from '@polkadot/keyring/types'
import { u8aToHex } from '@polkadot/util'
import { SocialEventDataApiInput } from '@subsocial/data-hub-sdk'
import { GraphQLClient, RequestOptions, Variables } from 'graphql-request'
import sortKeysRecursive from 'sort-keys-recursive'
import { datahubQueueConfig } from 'src/config/env'
import { getServerAccount } from '../common'

export function datahubQueueRequest<T, V extends Variables = Variables>(
config: RequestOptions<V, T>,
) {
const { url, token } = datahubQueueConfig || {}
if (!url) throw new Error('Datahub (Queue) config is not set')

const TIMEOUT = 3 * 1000 // 3 seconds
const client = new GraphQLClient(url, {
timeout: TIMEOUT,
headers: {
Authorization: `Bearer ${token}`,
},
...config,
})

return client.request({ url, ...config })
}

export function datahubMutationWrapper<T extends (...args: any[]) => Promise<any>>(func: T) {
return async (...args: Parameters<T>) => {
if (!datahubQueueConfig.url || !datahubQueueConfig.token) return
await func(...args)
}
}

function signDatahubPayload(signer: KeyringPair | null, payload: { sig: string }) {
if (!signer) throw new Error('Signer is not defined')
const sortedPayload = sortKeysRecursive(payload)
const sig = signer.sign(JSON.stringify(sortedPayload))
const hexSig = u8aToHex(sig)
payload.sig = hexSig
}
export const backendSigWrapper = async (input: SocialEventDataApiInput) => {
const signer = await getServerAccount()
if (!signer) throw new Error('Invalid Mnemonic')

input.providerAddr = signer.address
signDatahubPayload(signer, input)

return input
}
Loading

0 comments on commit a2e767a

Please sign in to comment.