Skip to content

Commit

Permalink
feat: support spaces follow and unfollow (#293)
Browse files Browse the repository at this point in the history
* feat: add placeholder page for followings

* feat: redirect logged user to timeline when on homepage

* feat: add proposals timeline for followed spaces

* fix: fix proposal links when outside space route

* fix: fix invalid argument type

* chore: remove unused import

* fix: fix missing redirection on internal page nav

* fix: handle network dynamically

* feat: show space name in proposals list

* fix: avoid "no proposals" message flashing while logging in

* fix: remove auto redirection to timeline

* fix(ux): link space name to space

* fix: resolve author domain name

* fix: fix ignored space showing props

* fix(ui): fix proposal title not being truncated

* fix: rename routes

* fix: add sidebar to timeline pages

* fix(ui): add back right sidebar

* chore: remove debug output

* fix(ui): fix icon alignment

* chore: put back line break

* refactor: rename function to avoid confusion

* chore: return early on empty arg

* fiix(ux): fix missing message when user has no followed spaces

* fix(ux): fix feed app nav on small screen

* fix(ui): show right sidebar on explore page

* refactor: DRY

* fix(ux): redirect guest user to landing when visiting /home

* fix: reset proposals on account change

* fix: highlighted voted proposals

* refactor: code improvement

* refactor: use nested routes for home and explore page

* chore: remove leftover

* fix: improve routes

* chore: remove extra DIV

* fix: fix appNav visibility on small screen

* fix(ui): show search form on home and explore page

* refactor: improve code

* refactor: delegate logged in user checking to parent route

* refactor: fix order

* refactor: use computed value

* refactor: upgrade Follow to first class citizen

* fix: add missing function to unsupported networks

* fix: skip timeline for unsupported wallet

* refactor: move `loadFollows` to `useAccount` composable

* refactor: improve typing

* feat: replace space's star button by follow/unfollow status

* refactor: move starredSpaces to useAccount

* refactor: rename variable for consistency

* fix: auto load followed spaces for connected account

* fix: keep sidebar order when mixing starred and followed spaces

* fix: fix sidebar spaces not loading for guest user

* feat: support custom order for multiple wallets

* rerfactor: code improvement

* fix: fix followed space status not being loaded

* refactor: DRY

* refactor: avoid using computed value when not depending on ref

* refactor: remove redundant condition

* fix: fix wrong typing

* refactor: avoid using computed value for simple values

* feat: support offchain spaces follow and unfollow

* fix: add follow/unfollow to all networks type

* chore: update changeset

* chore: add missing line break

* refactor: use simple spaceId instead of Space object

* fix(ui): show loading icon while toggling space following

* fix(ux): disable button while working

* fix(ui): allow sidebar scrolling

* fix: handle client_error thrown by sequencer

* fix(ui): make sidebar scrollable

* fix(ui): remove tooltip on follow/unfollow button

* fix(ui): add red border to unfollow button

* fix: remove favorite feature on offchain spaces for starknet accounts

* Merge branch 'master' into show-followed-spaces-in-sidebar

* fix: remove network from space Id

* fix: move bookmarks to a Store

* refactor: code improvement

* refactor: code DRYing

* fix: populate spaces store

* fix: fix spaces fetching for multiple network type

* fix: fix variable name

* chore: code cleaning

* fix(ux): add missing loading icon while loading follow list in sidebar

* chore: remove debug output

* fix: finish merge conflict

* fix: finish merge conflict

* fix: enable follow for offchain spaces

* fix: more refactoring

* fix: fix actions to always use offchain network

* fix: prevent event bubbling on follow button

* Update apps/ui/src/stores/followedSpaces.ts

Co-authored-by: Wiktor Tkaczyński <wiktor.tkaczynski@gmail.com>

---------

Co-authored-by: Wiktor Tkaczyński <wiktor.tkaczynski@gmail.com>
  • Loading branch information
wa0x6e and Sekhmet authored May 23, 2024
1 parent 3e8778c commit bdd30ea
Show file tree
Hide file tree
Showing 12 changed files with 224 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-dogs-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@snapshot-labs/sx": patch
---

add space follow and unfollow support for OffchainEthereumSig
17 changes: 13 additions & 4 deletions apps/ui/src/components/ButtonFollow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@ const props = defineProps<{
space: Space;
}>();
const spaceIdComposite = `${props.space.network}:${props.space.id}`;
const followedSpacesStore = useFollowedSpacesStore();
const spaceFollowed = computed(() =>
followedSpacesStore.isFollowed(`${props.space.network}:${props.space.id}`)
const spaceFollowed = computed(() => followedSpacesStore.isFollowed(spaceIdComposite));
const loading = computed(
() => !followedSpacesStore.followedSpacesLoaded || followedSpacesStore.followedSpaceLoading
);
</script>

<template>
<UiButton disabled class="group" :class="{ 'hover:border-skin-danger': spaceFollowed }">
<UiLoading v-if="!followedSpacesStore.followedSpacesLoaded" />
<UiButton
:disabled="loading"
class="group"
:class="{ 'hover:border-skin-danger': spaceFollowed }"
@click.prevent="followedSpacesStore.toggleSpaceFollow(spaceIdComposite)"
>
<UiLoading v-if="loading" />
<span v-else-if="spaceFollowed" class="inline-block">
<span class="group-hover:inline hidden text-skin-danger">Unfollow</span>
<span class="group-hover:hidden">Following</span>
Expand Down
51 changes: 49 additions & 2 deletions apps/ui/src/composables/useActions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
import { getNetwork, getReadWriteNetwork } from '@/networks';
import { enabledNetworks, getNetwork, getReadWriteNetwork, offchainNetworks } from '@/networks';
import { registerTransaction } from '@/helpers/mana';
import { convertToMetaTransactions } from '@/helpers/transactions';
import type {
Expand All @@ -14,6 +14,8 @@ import type {
} from '@/types';
import type { Connector, StrategyConfig } from '@/networks/types';

const offchainNetworkId = offchainNetworks.filter(network => enabledNetworks.includes(network))[0];

export function useActions() {
const { mixpanel } = useMixpanel();
const uiStore = useUiStore();
Expand Down Expand Up @@ -492,6 +494,49 @@ export function useActions() {
});
}

async function followSpace(networkId: NetworkID, spaceId: string) {
if (!web3.value.account) {
await forceLogin();
return false;
}

const network = getNetwork(offchainNetworkId);

try {
await wrapPromise(
offchainNetworkId,
network.actions.followSpace(auth.web3, networkId, spaceId)
);
} catch (e) {
console.log(e);
uiStore.addNotification('error', e.message);
return false;
}

return true;
}

async function unfollowSpace(networkId: NetworkID, spaceId: string) {
if (!web3.value.account) {
await forceLogin();
return false;
}

const network = getNetwork(offchainNetworkId);

try {
await wrapPromise(
offchainNetworkId,
network.actions.unfollowSpace(auth.web3, networkId, spaceId)
);
} catch (e) {
uiStore.addNotification('error', e.message);
return false;
}

return true;
}

return {
predictSpaceAddress: wrapWithErrors(predictSpaceAddress),
deployDependency: wrapWithErrors(deployDependency),
Expand All @@ -510,6 +555,8 @@ export function useActions() {
setMaxVotingDuration: wrapWithErrors(setMaxVotingDuration),
transferOwnership: wrapWithErrors(transferOwnership),
updateStrategies: wrapWithErrors(updateStrategies),
delegate: wrapWithErrors(delegate)
delegate: wrapWithErrors(delegate),
followSpace: wrapWithErrors(followSpace),
unfollowSpace: wrapWithErrors(unfollowSpace)
};
}
4 changes: 3 additions & 1 deletion apps/ui/src/networks/evm/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,8 @@ export function createActions(
};
})
);
}
},
followSpace: () => {},
unfollowSpace: () => {}
};
}
14 changes: 13 additions & 1 deletion apps/ui/src/networks/offchain/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getUrl } from '@/helpers/utils';
import { getProvider } from '@/helpers/provider';
import { getSwapLink } from '@/helpers/link';
import type { Web3Provider } from '@ethersproject/providers';
import type { StrategyParsedMetadata, Choice, Proposal, Space, VoteType } from '@/types';
import type { StrategyParsedMetadata, Choice, Proposal, Space, VoteType, NetworkID } from '@/types';
import type {
ReadOnlyNetworkActions,
NetworkConstants,
Expand Down Expand Up @@ -201,6 +201,18 @@ export function createActions(
swapLink: getSwapLink(strategy.name, strategy.params.address, strategy.network)
};
});
},
followSpace(web3: Web3Provider, networkId: NetworkID, spaceId: string) {
return client.followSpace({
signer: web3.getSigner(),
data: { network: networkId, space: spaceId }
});
},
unfollowSpace(web3: Web3Provider, networkId: NetworkID, spaceId: string) {
return client.unfollowSpace({
signer: web3.getSigner(),
data: { network: networkId, space: spaceId }
});
}
};
}
2 changes: 2 additions & 0 deletions apps/ui/src/networks/starknet/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,8 @@ export function createActions(
})
);
},
followSpace: () => {},
unfollowSpace: () => {},
send: (envelope: any) => starkSigClient.send(envelope) // TODO: extract it out of client to common helper
};
}
2 changes: 2 additions & 0 deletions apps/ui/src/networks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export type ReadOnlyNetworkActions = {
proposal: Proposal,
choice: Choice
): Promise<any>;
followSpace(web3: Web3Provider, networkId: NetworkID, spaceId: string);
unfollowSpace(web3: Web3Provider, networkId: NetworkID, spaceId: string);
send(envelope: any): Promise<any>;
};

Expand Down
34 changes: 34 additions & 0 deletions apps/ui/src/stores/followedSpaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ function getCompositeSpaceId(space: Space) {

export const useFollowedSpacesStore = defineStore('followedSpaces', () => {
const spacesStore = useSpacesStore();
const actions = useActions();
const { web3, authInitiated } = useWeb3();

const followedSpacesIds = ref<string[]>([]);
const followedSpacesLoaded = ref(false);
const followedSpaceLoading = ref(false);
const followedSpacesIdsByAccount = useStorage(
`${pkg.name}.spaces-followed`,
{} as Record<string, string[]>
Expand Down Expand Up @@ -83,6 +85,36 @@ export const useFollowedSpacesStore = defineStore('followedSpaces', () => {
fetchSpacesData(newIds);
}

async function toggleSpaceFollow(id: string) {
const alreadyFollowed = followedSpacesIds.value.includes(id);
const [spaceNetwork, spaceId] = id.split(':') as [NetworkID, string];
followedSpaceLoading.value = true;

try {
if (alreadyFollowed) {
const result = await actions.unfollowSpace(spaceNetwork, spaceId);
if (!result) return;

followedSpacesIds.value = followedSpacesIds.value.filter(
(spaceId: string) => spaceId !== id
);
followedSpacesIdsByAccount.value[web3.value.account] = followedSpacesIdsByAccount.value[
web3.value.account
].filter((spaceId: string) => spaceId !== id);
} else {
const result = await actions.followSpace(spaceNetwork, spaceId);
if (!result) return;

fetchSpacesData([id]);

followedSpacesIds.value.unshift(id);
followedSpacesIdsByAccount.value[web3.value.account].unshift(id);
}
} finally {
followedSpaceLoading.value = false;
}
}

function isFollowed(spaceId: string) {
return followedSpacesIds.value.includes(spaceId);
}
Expand Down Expand Up @@ -110,6 +142,8 @@ export const useFollowedSpacesStore = defineStore('followedSpaces', () => {
followedSpacesIds,
followedSpaceIdsByNetwork,
followedSpacesLoaded,
followedSpaceLoading,
toggleSpaceFollow,
isFollowed
};
});
5 changes: 3 additions & 2 deletions apps/ui/src/views/Space/Overview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ onMounted(() => {
proposalsStore.fetchSummary(props.space.id, props.space.network, PROPOSALS_LIMIT);
});
const spaceIdComposite = `${props.space.network}:${props.space.id}`;
const isOffchainSpace = offchainNetworks.includes(props.space.network);
const isController = computed(() => compareAddresses(props.space.controller, web3.value.account));
Expand All @@ -45,7 +44,9 @@ const socials = computed(() =>
.filter(social => social.href)
);
const proposalsRecord = computed(() => proposalsStore.proposals[spaceIdComposite]);
const proposalsRecord = computed(
() => proposalsStore.proposals[`${props.space.network}:${props.space.id}`]
);
const autolinkedAbout = computed(() =>
autolinker.link(props.space.about || '', {
Expand Down
48 changes: 45 additions & 3 deletions packages/sx.js/src/clients/offchain/ethereum-sig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
rankedChoiceVoteTypes,
weightedVoteTypes,
updateProposalTypes,
cancelProposalTypes
cancelProposalTypes,
followSpaceTypes,
unfollowSpaceTypes
} from './types';
import type { Signer, TypedDataSigner, TypedDataField } from '@ethersproject/abstract-signer';
import type {
Expand All @@ -20,11 +22,15 @@ import type {
Propose,
UpdateProposal,
CancelProposal,
FollowSpace,
UnfollowSpace,
EIP712Message,
EIP712VoteMessage,
EIP712ProposeMessage,
EIP712UpdateProposal,
EIP712CancelProposalMessage
EIP712CancelProposalMessage,
EIP712FollowSpaceMessage,
EIP712UnfollowSpaceMessage
} from '../types';
import type { OffchainNetworkConfig } from '../../../types';

Expand Down Expand Up @@ -53,6 +59,8 @@ export class EthereumSig {
| EIP712ProposeMessage
| EIP712UpdateProposal
| EIP712CancelProposalMessage
| EIP712FollowSpaceMessage
| EIP712UnfollowSpaceMessage
>(
signer: Signer & TypedDataSigner,
message: T,
Expand All @@ -76,7 +84,11 @@ export class EthereumSig {
};
}

public async send(envelope: Envelope<Vote | Propose | UpdateProposal | CancelProposal>) {
public async send(
envelope: Envelope<
Vote | Propose | UpdateProposal | CancelProposal | FollowSpace | UnfollowSpace
>
) {
const { address, signature: sig, domain, types, message } = envelope.signatureData!;
const payload = {
address,
Expand Down Expand Up @@ -212,4 +224,34 @@ export class EthereumSig {
data
};
}

public async followSpace({
signer,
data
}: {
signer: Signer & TypedDataSigner;
data: FollowSpace;
}): Promise<Envelope<FollowSpace>> {
const signatureData = await this.sign(signer, data, followSpaceTypes);

return {
signatureData,
data
};
}

public async unfollowSpace({
signer,
data
}: {
signer: Signer & TypedDataSigner;
data: UnfollowSpace;
}): Promise<Envelope<UnfollowSpace>> {
const signatureData = await this.sign(signer, data, unfollowSpaceTypes);

return {
signatureData,
data
};
}
}
18 changes: 18 additions & 0 deletions packages/sx.js/src/clients/offchain/ethereum-sig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,21 @@ export const cancelProposalTypes = {
{ name: 'proposal', type: 'bytes32' }
]
};

export const followSpaceTypes = {
Follow: [
{ name: 'from', type: 'address' },
{ name: 'network', type: 'string' },
{ name: 'space', type: 'string' },
{ name: 'timestamp', type: 'uint64' }
]
};

export const unfollowSpaceTypes = {
Unfollow: [
{ name: 'from', type: 'address' },
{ name: 'network', type: 'string' },
{ name: 'space', type: 'string' },
{ name: 'timestamp', type: 'uint64' }
]
};
Loading

0 comments on commit bdd30ea

Please sign in to comment.