Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add members space page #971

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
35 changes: 29 additions & 6 deletions apps/ui/src/components/App/Nav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import IHCog from '~icons/heroicons-outline/cog';
import IHGlobeAlt from '~icons/heroicons-outline/globe-alt';
import IHGlobe from '~icons/heroicons-outline/globe-americas';
import IHHome from '~icons/heroicons-outline/home';
import IHIdentification from '~icons/heroicons-outline/identification';
import IHLightningBolt from '~icons/heroicons-outline/lightning-bolt';
import IHNewspaper from '~icons/heroicons-outline/newspaper';
import IHStop from '~icons/heroicons-outline/stop';
Expand Down Expand Up @@ -42,15 +43,17 @@ const space = computed(() =>
: null
);

const isController = computedAsync(async () => {
if (!networkId.value || !space.value) return false;

const { account } = web3.value;
const controller = computedAsync(async () => {
if (!networkId.value || !space.value) return null;

const network = getNetwork(networkId.value);
const controller = await network.helpers.getSpaceController(space.value);
return await network.helpers.getSpaceController(space.value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return await network.helpers.getSpaceController(space.value);
return network.helpers.getSpaceController(space.value);

await there does nothing.

});

return compareAddresses(controller, account);
const isController = computed(() => {
const { account } = web3.value;

return compareAddresses(controller.value ?? '', account);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. I'd just check if it's undefined as separate case to avoid calling compareAddresses with fallback value.

});

const canSeeSettings = computed(() => {
Expand All @@ -63,6 +66,18 @@ const canSeeSettings = computed(() => {

return admins.includes(web3.value.account.toLowerCase());
}
return false;
});

const canSeeMembers = computed(() => {
const data = space.value?.additionalRawData;

return (
controller.value ||
(data?.admins?.length ?? 0) > 0 ||
(data?.moderators?.length ?? 0) > 0 ||
(data?.members?.length ?? 0) > 0
);
Comment on lines +72 to +80
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes this section visible on Snapshot onchain spaces, original issue includes this:

The page should only appear if there are members set on the space (only appear on Snapshot spaces and not SX).

Might be confusing though (Snapshot spaces = Snapshot offchain spaces, SX = Snapshot onchain spaces).

});

const navigationConfig = computed<
Expand All @@ -81,6 +96,14 @@ const navigationConfig = computed<
name: 'Leaderboard',
icon: IHUserGroup
},
...(canSeeMembers.value
? {
members: {
name: 'Members',
icon: IHIdentification
}
}
: undefined),
...(space.value?.delegations && space.value.delegations.length > 0
? {
delegates: {
Expand Down
23 changes: 23 additions & 0 deletions apps/ui/src/networks/offchain/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
RANKING_QUERY,
SPACE_QUERY,
SPACES_QUERY,
STATEMENTS_AND_USERS_QUERY,
STATEMENTS_QUERY,
STRATEGIES_QUERY,
STRATEGY_QUERY,
Expand Down Expand Up @@ -765,6 +766,28 @@ export function createApi(

return statements;
},
loadStatementsAndUsers: async (
networkId: NetworkID,
spaceId: string,
userIds: string[]
): Promise<{ statements: Statement[]; users: User[] }> => {
const { data } = await apollo.query({
query: STATEMENTS_AND_USERS_QUERY,
variables: {
where: {
delegate_in: userIds,
network: networkId,
space: spaceId
},
userIds
}
});

return {
statements: data.statements,
users: data.users
};
},
loadStrategies: async () => {
const { data } = await apollo.query({
query: STRATEGIES_QUERY
Expand Down
15 changes: 15 additions & 0 deletions apps/ui/src/networks/offchain/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,18 @@ export const STRATEGY_QUERY = gql`
}
${STRATEGY_FRAGMENT}
`;

export const STATEMENTS_AND_USERS_QUERY = gql`
query ($where: StatementsWhere, $userIds: [String!]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
query ($where: StatementsWhere, $userIds: [String!]) {
query ($statementsWhere: StatementsWhere, $userIds: [String!]) {

This way it's clearer when we execute query as to what where refers.

statements(where: $where) {
delegate
space
network
statement
}
users(where: { id_in: $userIds }) {
id
about
}
}
`;
5 changes: 5 additions & 0 deletions apps/ui/src/networks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ export type NetworkApi = {
spaceId: string,
userIds: string[]
): Promise<Statement[]>;
loadStatementsAndUsers(
networkId: NetworkID,
spaceId: string,
userIds: string[]
): Promise<{ statements: Statement[]; users: User[] }>;
loadStrategies(): Promise<StrategyTemplate[]>;
loadStrategy(address: string): Promise<StrategyTemplate | null>;
};
Expand Down
6 changes: 6 additions & 0 deletions apps/ui/src/routes/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import SpaceDelegates from '@/views/Space/Delegates.vue';
import SpaceDiscussions from '@/views/Space/Discussions.vue';
import SpaceEditor from '@/views/Space/Editor.vue';
import SpaceLeaderboard from '@/views/Space/Leaderboard.vue';
import SpaceMembers from '@/views/Space/Members.vue';
import SpaceOverview from '@/views/Space/Overview.vue';
import SpaceProposals from '@/views/Space/Proposals.vue';
import SpaceSearch from '@/views/Space/Search.vue';
Expand Down Expand Up @@ -71,6 +72,11 @@ export const spaceChildrenRoutes: RouteRecordRaw[] = [
name: 'space-leaderboard',
component: SpaceLeaderboard
},
{
path: 'members',
name: 'space-members',
component: SpaceMembers
},
{
path: 'profile/:user',
name: 'space-user',
Expand Down
123 changes: 123 additions & 0 deletions apps/ui/src/views/Space/Members.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<script setup lang="ts">
import removeMarkdown from 'remove-markdown';
import { getNames } from '@/helpers/stamp';
import { shorten } from '@/helpers/utils';
import { getNetwork } from '@/networks';
import { Space } from '@/types';

const props = defineProps<{ space: Space }>();

const { addNotification } = useUiStore();
const { setTitle } = useTitle();

const loaded = ref(false);
const roles = ref<{ data: string[]; label: string }[]>([]);
const names = ref<Record<string, string>>({});
const statements = ref<Record<string, string>>({});
const abouts = ref<Record<string, string>>({});
const network = computed(() => getNetwork(props.space.network));

const admins = props.space.additionalRawData?.admins || [];
const moderators = props.space.additionalRawData?.moderators || [];
const authors = props.space.additionalRawData?.members || [];

async function loadMembersData() {
try {
const controller = await network.value.helpers
.getSpaceController(props.space)
.then(c => c.toLowerCase());

const allUsers = [controller, ...admins, ...moderators, ...authors].map(u =>
u.toLowerCase()
);

names.value = await getNames(allUsers);

const { statements: userStatements, users } =
await network.value.api.loadStatementsAndUsers(
props.space.network,
props.space.id,
allUsers
);

statements.value = userStatements.reduce(
(acc, statement) => {
acc[statement.delegate.toLowerCase()] = statement.statement;
return acc;
},
{} as Record<string, string>
);

abouts.value = users.reduce(
(acc, user) => {
acc[user.id.toLowerCase()] = user.about || '';
return acc;
},
{} as Record<string, string>
);

roles.value = [
{ data: [controller], label: 'Controller' },
{ data: admins, label: 'Admin(s)' },
{ data: moderators, label: 'Moderator(s)' },
{ data: authors, label: 'Authors(s)' }
] as const;

loaded.value = true;
} catch (e) {
addNotification('error', 'Failed to load members data.');
loaded.value = true;
}
}
watchEffect(() => setTitle(`Members - ${props.space.name}`));

onMounted(loadMembersData);
</script>

<template>
<div v-if="!loaded" class="px-4 py-4 block">
<UiLoading class="block" />
</div>
<template v-for="role in roles" v-else :key="role.label">
<UiLabel v-if="role.data?.length" :label="role.label" />
<div class="text-left table-fixed w-full">
<div
v-for="(user, i) in role.data"
:key="`${role.label}-${i}`"
class="border-b flex space-x-1"
>
<div
class="flex items-center pl-4 py-3 gap-x-3 leading-[22px] min-w-[220px] truncate"
>
<UiStamp :id="user" :size="32" />
<AppLink
:to="{
name: 'space-user-statement',
params: { space: `${space.network}:${space.id}`, user: user }
}"
class="overflow-hidden"
>
<h4
class="text-skin-link truncate"
v-text="names[user] || shorten(user)"
/>
<div
class="text-[17px] text-skin-text truncate"
v-text="shorten(user)"
/>
</AppLink>
</div>
<div
class="hidden sm:flex items-center grow text-[17px] px-4 overflow-hidden leading-[22px] text-skin-text"
>
<span
v-if="statements[user] || abouts[user]"
class="line-clamp-2 max-h-[44px]"
>
{{ shorten(removeMarkdown(statements[user] || abouts[user]), 250) }}
</span>
</div>
</div>
</div>
</template>
</template>