Skip to content

Commit

Permalink
feat: add discourse user profile link (#1153)
Browse files Browse the repository at this point in the history
* feat: add discourse user profile link

* add address to error report
  • Loading branch information
andyesp committed Aug 7, 2023
1 parent a94703b commit 91e8133
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 10 deletions.
28 changes: 27 additions & 1 deletion src/back/routes/user.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { WithAuth, auth } from 'decentraland-gatsby/dist/entities/Auth/middleware'
import RequestError from 'decentraland-gatsby/dist/entities/Route/error'
import handleAPI from 'decentraland-gatsby/dist/entities/Route/handle'
import routes from 'decentraland-gatsby/dist/entities/Route/routes'
import { Request } from 'express'

import { isSameAddress } from '../../entities/Snapshot/utils'
import { GATSBY_DISCOURSE_CONNECT_THREAD, MESSAGE_TIMEOUT_TIME } from '../../entities/User/constants'
import UserModel from '../../entities/User/model'
import { ValidationMessage } from '../../entities/User/types'
import { UserAttributes, ValidationMessage } from '../../entities/User/types'
import { formatValidationMessage, getValidationComment, validateComment } from '../../entities/User/utils'
import { DiscourseService } from '../../services/DiscourseService'
import { ErrorService } from '../../services/ErrorService'
Expand All @@ -15,6 +16,7 @@ import { validateAddress } from '../utils/validations'
export default routes((route) => {
const withAuth = auth()
route.get('/user/:address/is-validated', handleAPI(isValidated))
route.get('/user/:address', handleAPI(getProfile))
route.get('/user/validate', withAuth, handleAPI(getValidationMessage))
route.post('/user/validate', withAuth, handleAPI(checkValidationMessage))
})
Expand Down Expand Up @@ -89,3 +91,27 @@ async function isValidated(req: Request) {
throw new Error(`${message}. ${error}`)
}
}

async function getProfile(req: Request) {
const address = req.params.address
validateAddress(address)

try {
const user = await UserModel.findOne<UserAttributes>({ address: address.toLowerCase() })

if (!user) {
throw new RequestError('User not found', RequestError.NotFound)
}

const { forum_id } = user

return {
forum_id,
forum_username: forum_id ? (await DiscourseService.getUserById(forum_id))?.username : null,
}
} catch (error) {
const message = 'Error while fetching profile data'
ErrorService.report(message, { address, error })
throw new Error(`${message}. ${error}`)
}
}
8 changes: 8 additions & 0 deletions src/clients/Discourse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@ export type DiscoursePostInTopic = {
reviewable_score_pending_count?: number
}

type DiscourseUser = {
username: string
}

export class Discourse extends API {
static Url = DISCOURSE_API || 'https://meta.discourse.org/'

Expand Down Expand Up @@ -368,4 +372,8 @@ export class Discourse extends API {
this.withAuth(this.options().method('GET'))
)
}

async getUserById(id: number) {
return this.fetch<DiscourseUser>(`/admin/users/${id}.json`, this.withAuth(this.options().method('GET')))
}
}
9 changes: 9 additions & 0 deletions src/clients/Governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,15 @@ export class Governance extends API {
return result.data
}

async getUserProfile(address: string) {
const result = await this.fetch<ApiResponse<{ forum_id: number | null; forum_username: string | null }>>(
`/user/${address}`,
this.options().method('GET')
)

return result.data
}

async getBadges(address: string) {
const response = await this.fetch<ApiResponse<UserBadges>>(`/badges/${address}`)
return response.data
Expand Down
4 changes: 2 additions & 2 deletions src/components/Icon/ValidatedProfile.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react'

function ValidatedProfile() {
function ValidatedProfile({ className }: { className?: string }) {
return (
<svg width="9" height="12" viewBox="0 0 9 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg className={className} width="9" height="12" viewBox="0 0 9 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M1.5 0C0.672656 0 0 0.672656 0 1.5V10.5C0 11.3273 0.672656 12 1.5 12H7.5C8.32734 12 9 11.3273 9 10.5V1.5C9 0.672656 8.32734 0 7.5 0H1.5ZM3.75 7.5H5.25C6.28594 7.5 7.125 8.33906 7.125 9.375C7.125 9.58125 6.95625 9.75 6.75 9.75H2.25C2.04375 9.75 1.875 9.58125 1.875 9.375C1.875 8.33906 2.71406 7.5 3.75 7.5ZM3 5.25C3 4.85218 3.15804 4.47064 3.43934 4.18934C3.72064 3.90804 4.10218 3.75 4.5 3.75C4.89782 3.75 5.27936 3.90804 5.56066 4.18934C5.84196 4.47064 6 4.85218 6 5.25C6 5.64782 5.84196 6.02936 5.56066 6.31066C5.27936 6.59196 4.89782 6.75 4.5 6.75C4.10218 6.75 3.72064 6.59196 3.43934 6.31066C3.15804 6.02936 3 5.64782 3 5.25ZM3.375 1H5.625C5.83125 1 6 1.29375 6 1.5C6 1.70625 5.83125 2 5.625 2H3.375C3.16875 2 3 1.70625 3 1.5C3 1.29375 3.16875 1 3.375 1Z"
fill="url(#paint0_linear_10713_3365)"
Expand Down
10 changes: 5 additions & 5 deletions src/components/User/UserStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import { Mobile, NotMobile } from 'decentraland-ui/dist/components/Media/Media'
import { VpDistribution } from '../../clients/SnapshotGraphqlTypes'
import { isSameAddress } from '../../entities/Snapshot/utils'
import useFormatMessage from '../../hooks/useFormatMessage'
import useIsProfileValidated from '../../hooks/useIsProfileValidated'
import ValidatedProfile from '../Icon/ValidatedProfile'
import useGovernanceProfile from '../../hooks/useGovernanceProfile'
import VotingPowerDistribution from '../Modal/VotingPowerDelegationDetail/VotingPowerDistribution'
import { ProfileBox } from '../Profile/ProfileBox'
import ProfileSettings from '../Profile/ProfileSettings'
Expand All @@ -20,6 +19,7 @@ import './UserStats.css'
import UserVotingStats from './UserVotingStats'
import UserVpStats from './UserVpStats'
import Username from './Username'
import ValidatedProfileCheck from './ValidatedProfileCheck'

interface Props {
address: string
Expand All @@ -31,9 +31,9 @@ const UserAvatar = React.lazy(() => import('./UserAvatar'))

export default function UserStats({ address, vpDistribution, isLoadingVpDistribution }: Props) {
const t = useFormatMessage()
const { isProfileValidated, validationChecked } = useIsProfileValidated(address)
const { profile, isLoadingGovernanceProfile, isProfileValidated } = useGovernanceProfile(address)
const [user] = useAuthContext()
const showSettings = isSameAddress(user, address) && validationChecked && !isProfileValidated
const showSettings = isSameAddress(user, address) && !isLoadingGovernanceProfile && !isProfileValidated
const { total } = vpDistribution || { total: 0 }

return (
Expand All @@ -46,7 +46,7 @@ export default function UserStats({ address, vpDistribution, isLoadingVpDistribu
<NotMobile>
<Username address={address} size="medium" className="UserStats__Username" />
</NotMobile>
{validationChecked && isProfileValidated && <ValidatedProfile />}
<ValidatedProfileCheck forumUsername={profile?.forum_username} isLoading={isLoadingGovernanceProfile} />
{showSettings && <ProfileSettings />}
</div>
<Badges address={address} />
Expand Down
10 changes: 10 additions & 0 deletions src/components/User/ValidatedProfileCheck.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.ValidatedProfileCheck {
display: flex;
position: relative;
width: 24px;
}

.ValidatedProfileCheck__Icon {
width: 100%;
height: 100%;
}
33 changes: 33 additions & 0 deletions src/components/User/ValidatedProfileCheck.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'

import { Loader } from 'decentraland-ui/dist/components/Loader/Loader'

import { forumUserUrl } from '../../entities/Proposal/utils'
import ValidatedProfile from '../Icon/ValidatedProfile'

import './ValidatedProfileCheck.css'

interface Props {
forumUsername?: string | null
isLoading: boolean
}

export default function ValidatedProfileCheck({ forumUsername, isLoading }: Props) {
if (isLoading) {
return (
<div className="ValidatedProfileCheck">
<Loader active size="tiny" />
</div>
)
}

return (
<>
{!isLoading && !!forumUsername && (
<a href={forumUserUrl(forumUsername)} target="_blank" rel="noreferrer" className="ValidatedProfileCheck">
<ValidatedProfile className="ValidatedProfileCheck__Icon" />
</a>
)}
</>
)
}
6 changes: 6 additions & 0 deletions src/entities/Proposal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ export function forumUrl(proposal: Pick<ProposalAttributes, 'discourse_topic_id'
return target.toString()
}

export function forumUserUrl(username: string) {
const target = new URL(DISCOURSE_API || '')
target.pathname = `/u/${username}`
return target.toString()
}

export function governanceUrl(pathname = '') {
const target = new URL(GOVERNANCE_API)
target.pathname = pathname
Expand Down
28 changes: 28 additions & 0 deletions src/hooks/useGovernanceProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useQuery } from '@tanstack/react-query'
import isEthereumAddress from 'validator/lib/isEthereumAddress'

import { ErrorClient } from '../clients/ErrorClient'
import { Governance } from '../clients/Governance'
import { ErrorCategory } from '../utils/errorCategories'

import { DEFAULT_QUERY_STALE_TIME } from './constants'

export default function useGovernanceProfile(address?: string | null) {
const { data, isLoading: isLoadingGovernanceProfile } = useQuery({
queryKey: [`userGovernanceProfile#${address?.toLowerCase()}`],
queryFn: async () => {
if (!address || !isEthereumAddress(address)) return null

try {
return await Governance.get().getUserProfile(address)
} catch (error) {
ErrorClient.report('Error getting governance profile', { error, address, category: ErrorCategory.Profile })
return null
}
},
staleTime: DEFAULT_QUERY_STALE_TIME,
enabled: !!address,
})

return { profile: data, isProfileValidated: !!data?.forum_id, isLoadingGovernanceProfile }
}
13 changes: 11 additions & 2 deletions src/services/DiscourseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class DiscourseService {
snapshotId: string
) {
try {
const discoursePost = await this.getDiscoursePost(data, profile, proposalId, snapshotUrl, snapshotId)
const discoursePost = await this.getPost(data, profile, proposalId, snapshotUrl, snapshotId)
const discourseProposal = await Discourse.get().createPost(discoursePost)
this.logProposalCreation(discourseProposal)
return discourseProposal
Expand All @@ -29,7 +29,7 @@ export class DiscourseService {
}
}

private static async getDiscoursePost(
private static async getPost(
data: ProposalInCreation,
profile: Avatar | null,
proposalId: string,
Expand Down Expand Up @@ -101,4 +101,13 @@ export class DiscourseService {
}
return allComments
}

static async getUserById(id: number) {
try {
return await Discourse.get().getUserById(id)
} catch (error) {
console.error(`Error getting Discourse user: ${id}`, error)
return null
}
}
}

0 comments on commit 91e8133

Please sign in to comment.