From 91e8133e2499e2baec35c48714bdd7e7aea2f9a9 Mon Sep 17 00:00:00 2001 From: Andy Espagnolo Date: Mon, 7 Aug 2023 12:16:24 -0300 Subject: [PATCH] feat: add discourse user profile link (#1153) * feat: add discourse user profile link * add address to error report --- src/back/routes/user.ts | 28 +++++++++++++++- src/clients/Discourse.ts | 8 +++++ src/clients/Governance.ts | 9 +++++ src/components/Icon/ValidatedProfile.tsx | 4 +-- src/components/User/UserStats.tsx | 10 +++--- src/components/User/ValidatedProfileCheck.css | 10 ++++++ src/components/User/ValidatedProfileCheck.tsx | 33 +++++++++++++++++++ src/entities/Proposal/utils.ts | 6 ++++ src/hooks/useGovernanceProfile.ts | 28 ++++++++++++++++ src/services/DiscourseService.ts | 13 ++++++-- 10 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 src/components/User/ValidatedProfileCheck.css create mode 100644 src/components/User/ValidatedProfileCheck.tsx create mode 100644 src/hooks/useGovernanceProfile.ts diff --git a/src/back/routes/user.ts b/src/back/routes/user.ts index e5dac5460..d21b6e8db 100644 --- a/src/back/routes/user.ts +++ b/src/back/routes/user.ts @@ -1,4 +1,5 @@ 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' @@ -6,7 +7,7 @@ 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' @@ -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)) }) @@ -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({ 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}`) + } +} diff --git a/src/clients/Discourse.ts b/src/clients/Discourse.ts index b8a4071e8..b5c6a5c19 100644 --- a/src/clients/Discourse.ts +++ b/src/clients/Discourse.ts @@ -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/' @@ -368,4 +372,8 @@ export class Discourse extends API { this.withAuth(this.options().method('GET')) ) } + + async getUserById(id: number) { + return this.fetch(`/admin/users/${id}.json`, this.withAuth(this.options().method('GET'))) + } } diff --git a/src/clients/Governance.ts b/src/clients/Governance.ts index cb9cb036d..848acbe19 100644 --- a/src/clients/Governance.ts +++ b/src/clients/Governance.ts @@ -466,6 +466,15 @@ export class Governance extends API { return result.data } + async getUserProfile(address: string) { + const result = await this.fetch>( + `/user/${address}`, + this.options().method('GET') + ) + + return result.data + } + async getBadges(address: string) { const response = await this.fetch>(`/badges/${address}`) return response.data diff --git a/src/components/Icon/ValidatedProfile.tsx b/src/components/Icon/ValidatedProfile.tsx index ba494da79..f2ce7cb47 100644 --- a/src/components/Icon/ValidatedProfile.tsx +++ b/src/components/Icon/ValidatedProfile.tsx @@ -1,8 +1,8 @@ import React from 'react' -function ValidatedProfile() { +function ValidatedProfile({ className }: { className?: string }) { return ( - + 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 ( @@ -46,7 +46,7 @@ export default function UserStats({ address, vpDistribution, isLoadingVpDistribu - {validationChecked && isProfileValidated && } + {showSettings && } diff --git a/src/components/User/ValidatedProfileCheck.css b/src/components/User/ValidatedProfileCheck.css new file mode 100644 index 000000000..575a3de76 --- /dev/null +++ b/src/components/User/ValidatedProfileCheck.css @@ -0,0 +1,10 @@ +.ValidatedProfileCheck { + display: flex; + position: relative; + width: 24px; +} + +.ValidatedProfileCheck__Icon { + width: 100%; + height: 100%; +} diff --git a/src/components/User/ValidatedProfileCheck.tsx b/src/components/User/ValidatedProfileCheck.tsx new file mode 100644 index 000000000..a555cc5a9 --- /dev/null +++ b/src/components/User/ValidatedProfileCheck.tsx @@ -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 ( +
+ +
+ ) + } + + return ( + <> + {!isLoading && !!forumUsername && ( + + + + )} + + ) +} diff --git a/src/entities/Proposal/utils.ts b/src/entities/Proposal/utils.ts index 40a1a68d7..a4e428a68 100644 --- a/src/entities/Proposal/utils.ts +++ b/src/entities/Proposal/utils.ts @@ -117,6 +117,12 @@ export function forumUrl(proposal: Pick { + 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 } +} diff --git a/src/services/DiscourseService.ts b/src/services/DiscourseService.ts index d7ea5f7ca..75a81cd53 100644 --- a/src/services/DiscourseService.ts +++ b/src/services/DiscourseService.ts @@ -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 @@ -29,7 +29,7 @@ export class DiscourseService { } } - private static async getDiscoursePost( + private static async getPost( data: ProposalInCreation, profile: Avatar | null, proposalId: string, @@ -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 + } + } }