Skip to content

Commit

Permalink
Merge pull request #383 from dappforce/deploy/global-moderation
Browse files Browse the repository at this point in the history
App Moderator Integration
  • Loading branch information
teodorus-nathaniel authored Mar 27, 2024
2 parents 6896adb + 6a23694 commit 61a1d4f
Show file tree
Hide file tree
Showing 30 changed files with 765 additions and 85 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ jobs:
GH_SERVER_MNEMONIC=${{ secrets.SERVER_MNEMONIC }}
GH_NEXT_PUBLIC_DATAHUB_QUERY_URL=https://sub-data-hub.subsocial.network/graphql
GH_NEXT_PUBLIC_DATAHUB_SUBSCRIPTION_URL=wss://sub-data-hub.subsocial.network/graphql-ws
GH_NEXT_PUBLIC_APP_ID=1
GH_DATAHUB_QUEUE_URL=https://sub-queue-data-hub.subsocial.network/graphql
GH_DATAHUB_QUEUE_TOKEN=${{ secrets.DATAHUB_QUEUE_TOKEN }}
# GH_NEXT_PUBLIC_ENABLE_MAINTENANCE_PAGE=true
Expand Down Expand Up @@ -86,6 +87,7 @@ jobs:
GH_SERVER_MNEMONIC=plunge pumpkin penalty segment cattle more print below fat lemon clap uniform
GH_NEXT_PUBLIC_DATAHUB_QUERY_URL=https://notifications-data-hub.subsocial.network/graphql
GH_NEXT_PUBLIC_DATAHUB_SUBSCRIPTION_URL=wss://notifications-data-hub.subsocial.network/graphql-ws
GH_NEXT_PUBLIC_APP_ID=12364
GH_DATAHUB_QUEUE_URL=https://notifications-queue-data-hub.subsocial.network/graphql
GH_DATAHUB_QUEUE_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZX0.jpXwkIJ4DpV4IvSI3eWVVXE6x89qr_GIq7IlbBv5YE0
# GH_NEXT_PUBLIC_ENABLE_MAINTENANCE_PAGE=true
Expand Down
1 change: 1 addition & 0 deletions ci.env
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ SELLER_CLIENT_ID='$GH_SELLER_CLIENT_ID'
SELLER_CLIENT_TOKEN_SIGNER='$GH_SELLER_TOKEN_SIGNER'
NEXT_PUBLIC_DATAHUB_QUERY_URL='GH_NEXT_PUBLIC_DATAHUB_QUERY_URL'
NEXT_PUBLIC_DATAHUB_SUBSCRIPTION_URL='GH_NEXT_PUBLIC_DATAHUB_SUBSCRIPTION_URL'
NEXT_PUBLIC_APP_ID='$GH_NEXT_PUBLIC_APP_ID'
DATAHUB_QUEUE_URL='GH_DATAHUB_QUEUE_URL'
DATAHUB_QUEUE_TOKEN='GH_DATAHUB_QUEUE_TOKEN'
SERVER_MNEMONIC='GH_SERVER_MNEMONIC'
Expand Down
5 changes: 5 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ ARG GH_SELLER_CLIENT_ID
ARG GH_SELLER_TOKEN_SIGNER
ARG GH_NEXT_PUBLIC_DATAHUB_QUERY_URL
ARG GH_NEXT_PUBLIC_DATAHUB_SUBSCRIPTION_URL
ARG GH_NEXT_PUBLIC_APP_ID
ARG GH_DATAHUB_QUEUE_URL
ARG GH_DATAHUB_QUEUE_TOKEN
ARG GH_SERVER_MNEMONIC
Expand All @@ -27,6 +28,7 @@ ENV NEXT_PUBLIC_GA_ID=${GH_GA_ID} \
SELLER_CLIENT_TOKEN_SIGNER=${GH_SELLER_TOKEN_SIGNER} \
NEXT_PUBLIC_DATAHUB_QUERY_URL=${GH_NEXT_PUBLIC_DATAHUB_QUERY_URL} \
NEXT_PUBLIC_DATAHUB_SUBSCRIPTION_URL=${GH_NEXT_PUBLIC_DATAHUB_SUBSCRIPTION_URL} \
NEXT_PUBLIC_APP_ID=${GH_NEXT_PUBLIC_APP_ID} \
DATAHUB_QUEUE_URL=${GH_DATAHUB_QUEUE_URL} \
DATAHUB_QUEUE_TOKEN=${GH_DATAHUB_QUEUE_TOKEN} \
SERVER_MNEMONIC=${GH_SERVER_MNEMONIC} \
Expand Down Expand Up @@ -56,13 +58,15 @@ ARG GH_SELLER_CLIENT_ID
ARG GH_SELLER_TOKEN_SIGNER
ARG GH_NEXT_PUBLIC_DATAHUB_QUERY_URL
ARG GH_NEXT_PUBLIC_DATAHUB_SUBSCRIPTION_URL
ARG GH_NEXT_PUBLIC_APP_ID
ARG GH_DATAHUB_QUEUE_URL
ARG GH_DATAHUB_QUEUE_TOKEN
ARG GH_SERVER_MNEMONIC
ARG GH_NEXT_PUBLIC_ENABLE_MAINTENANCE_PAGE

ENV NEXT_PUBLIC_GA_ID=${GH_GA_ID} \
NEXT_PUBLIC_APP_KIND=${GH_APP_KIND} \
NEXT_PUBLIC_APP_ID=${GH_APP_ID} \
NEXT_PUBLIC_APP_BASE_URL=${GH_APP_BASE_URL} \
NEXT_PUBLIC_AMP_ID=${GH_AMP_ID} \
NEXT_PUBLIC_OFFCHAIN_SIGNER_URL=${GH_OFFCHAIN_SIGNER_URL} \
Expand All @@ -71,6 +75,7 @@ ENV NEXT_PUBLIC_GA_ID=${GH_GA_ID} \
SELLER_CLIENT_TOKEN_SIGNER=${GH_SELLER_TOKEN_SIGNER} \
NEXT_PUBLIC_DATAHUB_QUERY_URL=${GH_NEXT_PUBLIC_DATAHUB_QUERY_URL} \
NEXT_PUBLIC_DATAHUB_SUBSCRIPTION_URL=${GH_NEXT_PUBLIC_DATAHUB_SUBSCRIPTION_URL} \
NEXT_PUBLIC_APP_ID=${GH_NEXT_PUBLIC_APP_ID} \
DATAHUB_QUEUE_URL=${GH_DATAHUB_QUEUE_URL} \
DATAHUB_QUEUE_TOKEN=${GH_DATAHUB_QUEUE_TOKEN} \
SERVER_MNEMONIC=${GH_SERVER_MNEMONIC} \
Expand Down
2 changes: 2 additions & 0 deletions src/components/comments/ViewComment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { getShortTimeRelativeToNow } from 'src/utils/date'
import { activeStakingLinks } from 'src/utils/links'
import { useMyAddress } from '../auth/MyAccountsContext'
import { FormatBalance } from '../common/balances'
import SubTeamLabel from '../moderation/SubTeamLabel'
import { ShareDropdown } from '../posts/share/ShareDropdown'
import { useShouldRenderMinStakeAlert } from '../posts/view-post'
import { PostDropDownMenu } from '../posts/view-post/PostDropDownMenu'
Expand Down Expand Up @@ -144,6 +145,7 @@ export const InnerViewComment: FC<Props> = props => {
<Tooltip title='Original Poster'>OP</Tooltip>
</Tag>
)}
<SubTeamLabel address={ownerId} />
</div>
<MutedSpan>&middot;</MutedSpan>
<CustomLink href='/[spaceId]/[slug]' as={commentLink}>
Expand Down
17 changes: 17 additions & 0 deletions src/components/moderation/BlockedAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Alert } from 'antd'
import CustomLink from '../referral/CustomLink'
import { CONTENT_POLICY_LINK } from './utils'

export default function BlockedAlert({ customPrefix = 'This account' }: { customPrefix?: string }) {
return (
<Alert
type='warning'
message={
<span>
{customPrefix} was blocked due to a violation of{' '}
<CustomLink href={CONTENT_POLICY_LINK}>Grill&apos;s content policy.</CustomLink>
</span>
}
/>
)
}
32 changes: 32 additions & 0 deletions src/components/moderation/BlockedModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Button } from 'antd'
import CustomLink from '../referral/CustomLink'
import CustomModal from '../utils/CustomModal'
import { APPEAL_LINK, CONTENT_POLICY_LINK } from './utils'

export default function BlockedModal({ ...props }: { visible: boolean; onCancel: () => void }) {
return (
<CustomModal
{...props}
title={
<span>
Your account was blocked due to a violation of{' '}
<CustomLink href={CONTENT_POLICY_LINK}>Grill&apos;s content policy.</CustomLink>
</span>
}
subtitle='You are now restricted from taking actions on Grill, but can still manage your locked SUB, and use other applications running on the Subsocial network.'
>
<div className='GapNormal' style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<CustomLink href='/c/staking'>
<Button size='large' type='primary' className='w-100'>
Manage SUB
</Button>
</CustomLink>
<CustomLink href={APPEAL_LINK}>
<Button size='large' className='w-100'>
Appeal
</Button>
</CustomLink>
</div>
</CustomModal>
)
}
37 changes: 37 additions & 0 deletions src/components/moderation/SubTeamLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import clsx from 'clsx'
import { CSSProperties } from 'react'
import { useIsAdmin } from 'src/rtk/features/moderation/hooks'

export default function SubTeamLabel({
address,
className,
style,
}: {
address: string
className?: string
style?: CSSProperties
}) {
const isAdmin = useIsAdmin(address)
if (!isAdmin) return null

return (
<div
className={clsx('RoundedHuge', className)}
style={{ padding: '0.125rem 0.5rem', background: '#F8FAFC', ...style }}
>
<span
className='FontTiny'
style={{
color: 'transparent',
display: 'block',
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
backgroundImage: 'linear-gradient(94deg, #FB339E 3.57%, #DB35F8 102.73%)',
WebkitTextFillColor: 'transparent',
}}
>
SUB Team
</span>
</div>
)
}
2 changes: 2 additions & 0 deletions src/components/moderation/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const CONTENT_POLICY_LINK = '/legal/content-policy'
export const APPEAL_LINK = '/c/appeal'
10 changes: 10 additions & 0 deletions src/components/posts/ModerateButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useIsAdmin } from 'src/rtk/features/moderation/hooks'
import { useModerationContext } from './ModerationProvider'

export default function ModerateButton({ postId }: { postId: string }) {
const isAdmin = useIsAdmin()
const { openModal } = useModerationContext()
if (!isAdmin) return null

return <div onClick={() => openModal(postId)}>Moderate</div>
}
65 changes: 65 additions & 0 deletions src/components/posts/ModerationProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
import { useIsAdmin } from 'src/rtk/features/moderation/hooks'
import { parseGrillMessage } from 'src/utils/iframe'
import { useMyAddress } from '../auth/MyAccountsContext'
import GrillIframeModal from '../utils/GrillIframe'

type State = {
openModal: (postId: string) => void
}

const Context = React.createContext<State>({ openModal: () => undefined })

export default function ModerationProvider({ children }: { children: ReactNode }) {
const [isOpenModal, setIsOpenModal] = useState(false)
const iframeRef = useRef<HTMLIFrameElement | null>(null)
const myAddress = useMyAddress()
const isAdmin = useIsAdmin(myAddress)

useEffect(() => {
const listener = (event: MessageEvent<any>) => {
const message = parseGrillMessage(event.data + '')
if (!message) return

const { name, value } = message
if (name === 'moderation' && value === 'close') {
setIsOpenModal(false)
}
}
window.addEventListener('message', listener)
return () => window.removeEventListener('message', listener)
}, [])

const value = useMemo(() => {
return {
openModal: (postId: string) => {
iframeRef.current?.contentWindow?.postMessage(
{
type: 'grill:moderate',
payload: postId,
},
'*',
)
setIsOpenModal(true)
},
}
}, [])

return (
<Context.Provider value={value}>
{children}
{isAdmin && (
<GrillIframeModal
pathname='/widget/moderation'
style={{ zIndex: 20 }}
isOpen={isOpenModal}
ref={iframeRef}
/>
)}
</Context.Provider>
)
}

export function useModerationContext() {
return React.useContext(Context)
}
12 changes: 11 additions & 1 deletion src/components/posts/view-post/PostDropDownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { ViewOnIpfs } from 'src/components/utils'
import { ButtonLink } from 'src/components/utils/CustomLinks'
import { DropdownMenu } from 'src/components/utils/DropDownMenu'
import { useSendEvent } from 'src/providers/AnalyticContext'
import { useIsAdmin } from 'src/rtk/features/moderation/hooks'
import { PostData, SpaceStruct } from 'src/types'
import { useIsMyAddress, useMyAddress } from '../../auth/MyAccountsContext'
import HiddenPostButton from '../HiddenPostButton'
import ModerateButton from '../ModerateButton'
import MovePostLink from '../MovePostLink'

type DropdownProps = {
Expand All @@ -28,6 +30,7 @@ const InnerPostDropDownMenu: FC<DropdownProps> = props => {
const { struct } = post
const postId = struct.id
const sendEvent = useSendEvent()
const isAdmin = useIsAdmin()

const { canEditPost, canHidePost, canMovePost } = useCheckCanEditAndHideSpacePermission(props)

Expand All @@ -54,6 +57,11 @@ const InnerPostDropDownMenu: FC<DropdownProps> = props => {
)}
</Menu.Item>
)}
{isAdmin && (
<Menu.Item key={`moderate-${postId}`}>
<ModerateButton postId={postId} />
</Menu.Item>
)}
{canHidePost && (
<Menu.Item key={`hidden-${postId}`}>
<HiddenPostButton post={struct} asLink />
Expand All @@ -75,13 +83,15 @@ const InnerPostDropDownMenu: FC<DropdownProps> = props => {
</Menu.Item>
</>
)
}, [myAddress, canEditPost, canHidePost, canMovePost])
}, [myAddress, canEditPost, canHidePost, canMovePost, isAdmin])

return <DropdownMenu buildMenuItems={buildMenuItems} {...otherProps} />
}

const PostDropDown: FC<DropdownProps> = props => {
const [stub, setStub] = useState(true)
// prefetch admin data
useIsAdmin()

const closeStub = () => setStub(false)

Expand Down
18 changes: 12 additions & 6 deletions src/components/posts/view-post/PostPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ import { return404 } from 'src/components/utils/next'
import Segment from 'src/components/utils/Segment'
import config from 'src/config'
import { POST_VIEW_DURATION } from 'src/config/constants'
import { appId } from 'src/config/env'
import { resolveIpfsUrl } from 'src/ipfs'
import { getInitialPropsWithRedux, NextContextWithRedux } from 'src/rtk/app'
import { useSelectProfile } from 'src/rtk/app/hooks'
import { useAppDispatch, useAppSelector } from 'src/rtk/app/store'
import { fetchPostRewards } from 'src/rtk/features/activeStaking/postRewardSlice'
import { fetchTopUsersWithSpaces } from 'src/rtk/features/leaderboard/topUsersSlice'
import { fetchBlockedResources } from 'src/rtk/features/moderation/blockedResourcesSlice'
import { useIsPostBlocked } from 'src/rtk/features/moderation/hooks'
import { fetchPost, fetchPosts, selectPost } from 'src/rtk/features/posts/postsSlice'
import { fetchPostsViewCount } from 'src/rtk/features/posts/postsViewCountSlice'
import { useFetchMyReactionsByPostId } from 'src/rtk/features/reactions/myPostReactionsHooks'
Expand Down Expand Up @@ -62,6 +65,8 @@ const InnerPostPage: NextPage<PostDetailsProps> = props => {
const id = initialPostData.id
const { isNotMobile } = useResponsiveSize()
useFetchMyReactionsByPostId(id)
// data prefetched
const { isBlocked } = useIsPostBlocked(initialPostData.post.struct)

const postData = useAppSelector(state => selectPost(state, { id })) || initialPostData

Expand All @@ -79,7 +84,7 @@ const InnerPostPage: NextPage<PostDetailsProps> = props => {

const isUnlistedPost = useIsUnlistedPost({ post: struct, space: space?.struct })

if (useIsUnlistedSpace(postData.space) || isUnlistedPost) return <PostNotFoundPage />
if (useIsUnlistedSpace(postData.space) || isUnlistedPost || isBlocked) return <PostNotFoundPage />

if (!content) return null

Expand Down Expand Up @@ -297,11 +302,12 @@ export async function loadPostOnNextReq({

if (!postId) return return404(context)

const replyIds = await blockchain.getReplyIdsByPostId(idToBn(postId))

const ids = replyIds.concat(postId)

await dispatch(fetchPosts({ api: subsocial, ids, reload: true, eagerLoadHandles: true }))
async function getPost() {
const replyIds = await blockchain.getReplyIdsByPostId(idToBn(postId!))
const ids = replyIds.concat(postId!)
await dispatch(fetchPosts({ api: subsocial, ids, reload: true, eagerLoadHandles: true }))
}
await Promise.all([getPost(), dispatch(fetchBlockedResources({ appId }))])
const postData = selectPost(reduxStore.getState(), { id: postId })

if (!postData?.space) return return404(context)
Expand Down
5 changes: 4 additions & 1 deletion src/components/posts/view-post/PostPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useMyAddress } from 'src/components/auth/MyAccountsContext'
import { addPostView } from 'src/components/utils/datahub/post-view'
import { Segment } from 'src/components/utils/Segment'
import { POST_VIEW_DURATION } from 'src/config/constants'
import { useIsPostBlocked } from 'src/rtk/features/moderation/hooks'
import { asSharedPostStruct, PostWithAllDetails, PostWithSomeDetails, SpaceData } from 'src/types'
import {
HiddenPostAlert,
Expand Down Expand Up @@ -56,6 +57,8 @@ export function PostPreview(props: PreviewProps) {
const isUnlisted = useIsUnlistedPost({ post, space: space?.struct })
const isHiddenChatRoom =
shouldHideChatRooms && HIDE_PREVIEW_FROM_SPACE.includes(post.spaceId ?? '')
// data is prefetched from the getPosts in postsSlice
const { isBlocked } = useIsPostBlocked(post)

const { inView, ref } = useInView()
useEffect(() => {
Expand Down Expand Up @@ -91,7 +94,7 @@ export function PostPreview(props: PreviewProps) {
}
}, [inView, myAddress])

if (isUnlisted || isHiddenChatRoom) return null
if (isUnlisted || isHiddenChatRoom || isBlocked) return null

const postContent = postDetails.post.content
const isEmptyContent = !isSharedPost && !postContent?.title && !postContent?.body
Expand Down
Loading

0 comments on commit 61a1d4f

Please sign in to comment.