Skip to content

Commit

Permalink
feat: space user profile page (#407)
Browse files Browse the repository at this point in the history
* fix: add user profile page to My sidebar

* fix(ui): update the user profile page to the new UI

* fix: use offchain network as user profile source of truth

* feat: add activities to user profile

* fix: add `avatar` to User

* chore: remove invalid import

* fix: fix font weight

* fix: style improvement

* feat: add edit profile modal

* fix: fix typing

* fix: account modal redirect to profile page instead of etherscan

* feat: send user update action to offchain network

* fix: fix network signature

* fix: support user not existing on offchain network

* fix: default username to resolve domains when not set

* feat: save user profile to offchain network

* fix: bust cache on avatar update

* fix: fix invalid condition

* fix: add share dropdown

* fix: page content should reflect url

* chore: fix function ordering

* feat: allow profile edition via alias

* fix: fix typing

* chore: reorder import

* chore : fix import

* fix: rename Profile to User

* refactor: code DRYing

* refactor: remove non-valid type property

* refactor: DRY-ing social networks links

* refactor: DRY-ing the share dropdown

* fix: add max length to input

* fix: keep same alignment as existing pages

* feat: add copy link to share dropdown

* fix: fix avatar rounding

* fix: make profile active only on own profile

* fix: refresh user activities on internal navigation

* fix: use username in page title

* fix: fix class being ignored

* fix: hide some menu for guest user

* fix: fix menu label

* fix: remove uneeded import

* fix: show original image until stamp support user cover

* refactor: avoid using extra params

* refactor: remove duplicate code

* chore: fix declaration order

* fix: return resolvedName from network api if available

* fix: follow User signature

* fix: make values optional

* fix: fix not used var

* fix: remove unecessary type

* refactor: code improvement

* refactor: pass user directly instead of pinning

* fix: improve typing

* fix: remove cache clearing, now delegated to backend

* feat: show username on topnav account menu when available

* fix: update User profile share message

* fix: fix empty nav sidebar appearing on all other pages

* fix: use UserCover image from stamp

* chore: update changelog

* fix: fix undefined user flashing

* refactor: rename component to follow convention

* refactor: improve aggregation

* fix: user helper function to return `cb`

* refactor: improve user fetching when not existing on backend

* refactor: DRY

* fix: fix UserCover not fetching usercover type

* chore: remove debug output

* feat: add space user profile page skeleton

* chore: remove unused import

* fix: fix typing

* fix: fix typing

* fix: move statement loading inside SpaceUserOverview

* feat: support statement edition

* fix: show user's voting power

* fix: fix wrong text color

* fix: user profile links should redirect to space user profile when inside a space

* fix: use better vp formatting

* fix: remove unready discourse properties

* fix: fix division by zero

* fix: make whole row clickable

* fix: fix status label

* fix: update statement via alias

* fix: fix types

* fix: fix type

* fix: use separate variable for user and userActivity

* fix: fix typing

* Update packages/sx.js/src/clients/offchain/ethereum-sig/index.ts

Co-authored-by: Chaitanya <[email protected]>

* fix: rename label to follow figma

* fix: remove extraneous computed wrapper

* refactor: remove deprecated method

* chore: add changeset

* fix: fix wrong link and user id

* fix: show delegators count

* fix: add User Space delegators page

* fix: add message on missing statement

* fix: only load delegators counter from main page

* Update apps/ui/src/views/SpaceUser.vue

* Update apps/ui/src/views/SpaceUser.vue

* fix: link to user space profile

* fix: load user leaderboard from offchain

* fix: remove duplicate fetchSpace call

* fix: clear values on user change

* Update apps/ui/src/views/SpaceUser.vue

Co-authored-by: Wiktor Tkaczyński <[email protected]>

* refactor: remove unecessary async

* chore: lint fix

* fix: comment unused code

* fix: use metadataNetwork for metadata saving

* chore: yarn lint

---------

Co-authored-by: Chaitanya <[email protected]>
Co-authored-by: Wiktor Tkaczyński <[email protected]>
  • Loading branch information
3 people authored Jul 24, 2024
1 parent 71ca75a commit 6c95391
Show file tree
Hide file tree
Showing 28 changed files with 765 additions and 99 deletions.
5 changes: 5 additions & 0 deletions .changeset/shiny-eggs-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@snapshot-labs/sx": patch
---

add updateStatement to offchain ethereum-sig
115 changes: 115 additions & 0 deletions apps/ui/src/components/EditorStatement.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<script setup lang="ts">
import { clone } from '@/helpers/utils';
import { getValidator } from '@/helpers/validation';
import { Statement } from '@/types';
const model = defineModel<Statement>({
required: true
});
const emit = defineEmits<{
(e: 'close');
}>();
const actions = useActions();
const sending = ref(false);
const previewEnabled = ref(false);
const form = reactive(clone(model.value));
const formErrors = ref({} as Record<string, any>);
const formValidated = ref(false);
const STATUS_DEFINITION = {
enum: ['ACTIVE', 'INACTIVE'],
options: [
{ id: 'ACTIVE', name: 'Active' },
{ id: 'INACTIVE', name: 'Inactive' }
],
title: 'Status'
};
const formValidator = getValidator({
$async: true,
type: 'object',
title: 'Statement',
additionalProperties: false,
required: [],
properties: {
statement: {
type: 'string',
format: 'long',
title: 'Statement',
maxLength: 10000
},
status: STATUS_DEFINITION
}
});
async function handleSubmit() {
sending.value = true;
try {
await actions.updateStatement(form);
model.value = form;
emit('close');
} finally {
sending.value = false;
}
}
watchEffect(() => {
formValidated.value = false;
formErrors.value = formValidator.validate({
statement: form.statement,
status: form.status
});
formValidated.value = true;
});
</script>

<template>
<div class="max-w-[592px] s-box">
<UiSelect
v-model="form.status"
:definition="STATUS_DEFINITION"
:error="formErrors.status"
/>
<div class="mb-3">
<div class="flex space-x-3">
<button type="button" @click="previewEnabled = false">
<UiLink
:is-active="!previewEnabled"
text="Write"
class="border-transparent"
/>
</button>
<button type="button" @click="previewEnabled = true">
<UiLink
:is-active="previewEnabled"
text="Preview"
class="border-transparent"
/>
</button>
</div>
<UiMarkdown
v-if="previewEnabled"
class="px-3 py-2 mb-3 border rounded-lg min-h-[200px]"
:body="form.statement"
/>
<UiComposer v-else v-model="form.statement" />
</div>
<div class="flex items-center justify-between space-x-2.5">
<UiButton class="w-full" @click="$emit('close')">Cancel</UiButton>
<UiButton
primary
class="w-full"
:disabled="!formValidated || Object.keys(formErrors).length > 0"
:loading="sending"
@click="handleSubmit"
>
Save
</UiButton>
</div>
</div>
</template>
5 changes: 3 additions & 2 deletions apps/ui/src/components/Modal/Votes.vue
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,10 @@ watch(
/>
<router-link
:to="{
name: 'user',
name: 'space-user-statement',
params: {
id: vote.voter.id
id: `${proposal.network}:${proposal.space.id}`,
user: vote.voter.id
}
}"
class="grow flex space-x-2 items-center"
Expand Down
7 changes: 5 additions & 2 deletions apps/ui/src/components/ProposalsListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,11 @@ async function handleVoteClick(choice: Choice) {
<router-link
class="text-skin-text"
:to="{
name: 'user',
params: { id: proposal.author.id }
name: 'space-user-statement',
params: {
id: `${proposal.network}:${proposal.space.id}`,
user: proposal.author.id
}
}"
>
{{ proposal.author.name || shortenAddress(proposal.author.id) }}
Expand Down
20 changes: 10 additions & 10 deletions apps/ui/src/components/SpaceDelegates.vue
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,14 @@ watchEffect(() => setTitle(`Delegates - ${props.space.name}`));
>
<div class="flex items-center w-[60%] pl-4 py-3 gap-x-3 truncate">
<UiStamp :id="delegate.user" :size="32" />
<a
:href="
currentNetwork.helpers.getExplorerUrl(
delegate.user,
'address'
)
"
target="_blank"
<router-link
:to="{
name: 'space-user-statement',
params: {
id: `${space.network}:${space.id}`,
user: delegate.user
}
}"
class="overflow-hidden leading-[22px]"
>
<h4
Expand All @@ -165,9 +165,9 @@ watchEffect(() => setTitle(`Delegates - ${props.space.name}`));
/>
<div
class="text-[17px] text-skin-text truncate"
v-text="shorten(delegate.user)"
v-text="shorten(delegate.id)"
/>
</a>
</router-link>
</div>
<div
class="hidden md:flex w-[20%] flex-col items-end justify-center leading-[22px] truncate"
Expand Down
19 changes: 18 additions & 1 deletion apps/ui/src/composables/useActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Space,
SpaceMetadata,
SpaceSettings,
Statement,
Transaction,
User,
VoteType
Expand Down Expand Up @@ -622,6 +623,21 @@ export function useActions() {
return true;
}

async function updateStatement(statement: Statement) {
const network = getNetwork(metadataNetwork);

await wrapPromise(
metadataNetwork,
network.actions.updateStatement(
await getAliasSigner(),
statement,
web3.value.account
)
);

return true;
}

return {
predictSpaceAddress: wrapWithErrors(predictSpaceAddress),
deployDependency: wrapWithErrors(deployDependency),
Expand All @@ -643,6 +659,7 @@ export function useActions() {
delegate: wrapWithErrors(delegate),
followSpace: wrapWithErrors(followSpace),
unfollowSpace: wrapWithErrors(unfollowSpace),
updateUser: wrapWithErrors(updateUser)
updateUser: wrapWithErrors(updateUser),
updateStatement: wrapWithErrors(updateStatement)
};
}
98 changes: 56 additions & 42 deletions apps/ui/src/composables/useDelegates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type ApiDelegate = {
tokenHoldersRepresentedAmount: number;
};

type Delegate = ApiDelegate & {
export type Delegate = ApiDelegate & {
name: string | null;
delegatorsPercentage: number;
votesPercentage: number;
Expand All @@ -25,6 +25,22 @@ type Governance = {
totalDelegates: string;
};

type DelegatesQueryFilter = {
orderBy: string;
orderDirection: string;
skip: number;
first: number;
user?: string;
};

type SortOrder =
| 'delegatedVotes-desc'
| 'delegatedVotes-asc'
| 'tokenHoldersRepresentedAmount-desc'
| 'tokenHoldersRepresentedAmount-asc';

const DEFAULT_ORDER = 'delegatedVotes-desc';

const DELEGATES_LIMIT = 40;

const DELEGATES_QUERY = gql`
Expand All @@ -34,13 +50,18 @@ const DELEGATES_QUERY = gql`
$orderBy: Delegate_orderBy!
$orderDirection: OrderDirection!
$governance: String!
$user: String
) {
delegates(
first: $first
skip: $skip
orderBy: $orderBy
orderDirection: $orderDirection
where: { tokenHoldersRepresentedAmount_gte: 0, governance: $governance }
where: {
tokenHoldersRepresentedAmount_gte: 0
governance: $governance
user: $user
}
) {
id
user
Expand Down Expand Up @@ -91,34 +112,16 @@ export function useDelegates(delegationApiUrl: string, governance: string) {
}
});

async function _fetch(
overwrite: boolean,
sortBy:
| 'delegatedVotes-desc'
| 'delegatedVotes-asc'
| 'tokenHoldersRepresentedAmount-desc'
| 'tokenHoldersRepresentedAmount-asc'
) {
const [orderBy, orderDirection] = sortBy.split('-');

const { data } = await apollo.query({
query: DELEGATES_QUERY,
variables: {
orderBy,
orderDirection,
governance: governance.toLowerCase(),
first: DELEGATES_LIMIT,
skip: overwrite ? 0 : delegates.value.length
}
});

const governanceData = data.governance as Governance;
const delegatesData = data.delegates as ApiDelegate[];
async function formatDelegates(data: {
governance: Governance;
delegates: ApiDelegate[];
}): Promise<Delegate[]> {
const governanceData = data.governance;
const delegatesData = data.delegates;
const addresses = delegatesData.map(delegate => delegate.user);

const names = await getNames(addresses);

const newDelegates = delegatesData.map((delegate: ApiDelegate) => {
return delegatesData.map((delegate: ApiDelegate) => {
const delegatorsPercentage =
Number(delegate.tokenHoldersRepresentedAmount) /
Number(governanceData.totalDelegates);
Expand All @@ -133,21 +136,37 @@ export function useDelegates(delegationApiUrl: string, governance: string) {
votesPercentage
};
});
}

async function getDelegates(
filter: DelegatesQueryFilter
): Promise<Delegate[]> {
const { data } = await apollo.query({
query: DELEGATES_QUERY,
variables: { ...filter, governance: governance.toLowerCase() }
});

return formatDelegates(data);
}

async function _fetch(overwrite: boolean, sortBy: SortOrder) {
const [orderBy, orderDirection] = sortBy.split('-');

const newDelegates = await getDelegates({
orderBy,
orderDirection,
skip: overwrite ? 0 : delegates.value.length,
first: DELEGATES_LIMIT
});

delegates.value = overwrite
? newDelegates
: [...delegates.value, ...newDelegates];

hasMore.value = delegatesData.length === DELEGATES_LIMIT;
hasMore.value = newDelegates.length === DELEGATES_LIMIT;
}

async function fetch(
sortBy:
| 'delegatedVotes-desc'
| 'delegatedVotes-asc'
| 'tokenHoldersRepresentedAmount-desc'
| 'tokenHoldersRepresentedAmount-asc' = 'delegatedVotes-desc'
) {
async function fetch(sortBy: SortOrder = DEFAULT_ORDER) {
if (loading.value || loaded.value) return;
loading.value = true;

Expand All @@ -162,13 +181,7 @@ export function useDelegates(delegationApiUrl: string, governance: string) {
}
}

async function fetchMore(
sortBy:
| 'delegatedVotes-desc'
| 'delegatedVotes-asc'
| 'tokenHoldersRepresentedAmount-desc'
| 'tokenHoldersRepresentedAmount-asc' = 'delegatedVotes-desc'
) {
async function fetchMore(sortBy: SortOrder = DEFAULT_ORDER) {
if (loading.value || !loaded.value) return;
loadingMore.value = true;

Expand All @@ -193,6 +206,7 @@ export function useDelegates(delegationApiUrl: string, governance: string) {
failed,
hasMore,
delegates,
getDelegates,
fetch,
fetchMore,
reset
Expand Down
Loading

0 comments on commit 6c95391

Please sign in to comment.