Skip to content

Commit a04f72e

Browse files
committed
Update DAOs page to show "Your DAOs" first
Separate section at the top of the DAOs page with DAOs user is a member of or follows first. Then below that is an Other DAOs section with all other DAOs. Note that this will currently not show more than 100 DAOs that a user is a member of and 100 that they folllow. Depends on new client daostack/arc.js#443 Fixes #1581
1 parent a01d2a5 commit a04f72e

File tree

8 files changed

+172
-140
lines changed

8 files changed

+172
-140
lines changed

src/App.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class App extends React.Component<{}, {
2727
};
2828
}
2929

30-
30+
3131
private getPdfHtml = (filename: string): any => {
3232
window.location.href = `${window.location.protocol}//${window.location.host}/assets/${filename}`;
3333
return null;
@@ -77,7 +77,7 @@ export class App extends React.Component<{}, {
7777
await sleep(2000);
7878
}
7979
}
80-
80+
8181

8282
let GOOGLE_ANALYTICS_ID: string;
8383
switch (process.env.NODE_ENV) {
@@ -122,6 +122,7 @@ export class App extends React.Component<{}, {
122122
<Route path="/" exact component={AppContainer}/>
123123
<Route path="/dao" component={AppContainer}/>
124124
<Route path="/daos" component={AppContainer}/>
125+
<Route path="/feed" component={AppContainer}/>
125126
<Route path="/profile" component={AppContainer}/>
126127
<Route path="/redemptions" component={AppContainer}/>
127128
<Route path="/daos/create" component={AppContainer} />

src/components/Daos/Daos.scss

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@
4242
}
4343

4444
.createDaoButton {
45-
position: absolute;
45+
position: fixed;
4646
right: 20px;
47-
bottom: 5px;
47+
top: 100px;
48+
z-index: 100000000;
4849
border-radius: 15px;
4950
background-color: rgba(3, 118, 255, 1);
5051
color: $white;

src/components/Daos/DaosPage.tsx

Lines changed: 95 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { DAO } from "@daostack/client";
1+
import { DAO, DAOFieldsFragment } from "@daostack/client";
22
import { getArc } from "arc";
33
import Loading from "components/Shared/Loading";
44
import withSubscription, { ISubscriptionProps } from "components/Shared/withSubscription";
55
import gql from "graphql-tag";
66
import Analytics from "lib/analytics";
7+
import { createDaoStateFromQuery } from "lib/daoHelpers";
78
import { Page } from "pages";
89
import * as React from "react";
910
import { BreadcrumbsItem } from "react-breadcrumbs-dynamic";
@@ -12,24 +13,23 @@ import { connect } from "react-redux";
1213
import * as Sticky from "react-stickynode";
1314
import { Link } from "react-router-dom";
1415
import { IRootState } from "reducers";
15-
import { IProfileState } from "reducers/profilesReducer";
1616
import { combineLatest, of } from "rxjs";
1717
import { first } from "rxjs/operators";
1818

1919
import DaoCard from "./DaoCard";
2020
import * as css from "./Daos.scss";
2121

22-
type SubscriptionData = [DAO[], string[]];
22+
type SubscriptionData = [DAO[], DAO[], DAO[]];
2323

2424
interface IStateProps {
2525
currentAccountAddress: string;
26-
currentAccountProfile: IProfileState;
26+
followingDAOs: string[];
2727
}
2828

2929
const mapStateToProps = (state: IRootState): IStateProps => {
3030
return {
3131
currentAccountAddress: state.web3.currentAccountAddress,
32-
currentAccountProfile: state.profiles[state.web3.currentAccountAddress],
32+
followingDAOs: state.profiles[state.web3.currentAccountAddress] ? state.profiles[state.web3.currentAccountAddress].follows.daos : [],
3333
};
3434
};
3535

@@ -81,39 +81,46 @@ class DaosPage extends React.Component<IProps, IState> {
8181
}
8282

8383
public render(): RenderOutput {
84-
const { currentAccountProfile, data, fetchMore } = this.props;
84+
const { data, fetchMore } = this.props;
8585
const search = this.state.search.length > 2 ? this.state.search.toLowerCase() : "";
8686

87-
let allDAOs = data[0];
87+
// Always show DAOs that the current user is a member of or follows first
88+
const yourDAOs = data[1].concat(data[2]).filter(d => d.staticState.name.toLowerCase().includes(search)).sort((a, b) => a.staticState.name.localeCompare(b.staticState.name));
89+
const yourDAOAddresses = yourDAOs.map(dao => dao.id);
8890

91+
// Then all the rest of the DAOs
92+
let otherDAOs = data[0];
8993
// Add any DAOs found from searching the server to the list
9094
if (this.state.searchDaos.length > 0) {
9195
// make sure we don't add duplicate DAOs to the list
9296
const extraFoundDaos = this.state.searchDaos.filter((dao) => {
93-
return !allDAOs.find((d) => d.id === dao.id);
97+
return !otherDAOs.find((d) => d.id === dao.id);
9498
});
95-
allDAOs = allDAOs.concat(extraFoundDaos);
99+
otherDAOs = otherDAOs.concat(extraFoundDaos);
96100
}
97101

98-
// Always show Genesis Alpha first
99-
let finalDAOList = allDAOs.filter((d: DAO) => d.staticState.name === "Genesis Alpha" && d.staticState.name.toLowerCase().includes(search));
100-
101102
if (process.env.NODE_ENV === "staging") {
102103
// on staging we show all daos (registered or not)
103-
finalDAOList = finalDAOList.concat(allDAOs.filter((d: DAO) => d.staticState.name !== "Genesis Alpha" && d.staticState.name.toLowerCase().includes(search)));
104+
otherDAOs = otherDAOs.filter((d: DAO) => !yourDAOAddresses.includes(d.id) && d.staticState.name.toLowerCase().includes(search));
104105
} else {
105-
// Otherwise show registered DAOs or DAOs that the person follows or is a member of
106-
const memberOfDAOs = data[1];
107-
finalDAOList = finalDAOList.concat(allDAOs.filter((d: DAO) => {
108-
return d.staticState.name !== "Genesis Alpha" &&
106+
// Otherwise show registered DAOs
107+
otherDAOs = otherDAOs.filter((d: DAO) => {
108+
return !yourDAOAddresses.includes(d.id) &&
109109
d.staticState.name.toLowerCase().includes(search) &&
110-
(d.staticState.register === "registered" ||
111-
(currentAccountProfile && currentAccountProfile.follows.daos.includes(d.staticState.address)) ||
112-
memberOfDAOs.includes(d.staticState.address));
113-
}));
110+
d.staticState.register === "registered";
111+
});
114112
}
115113

116-
const daoNodes = finalDAOList.map((dao: DAO) => {
114+
const yourDaoNodes = yourDAOs.map((dao: DAO) => {
115+
return (
116+
<DaoCard
117+
key={dao.id}
118+
dao={dao}
119+
/>
120+
);
121+
});
122+
123+
const otherDaoNodes = otherDAOs.map((dao: DAO) => {
117124
return (
118125
<DaoCard
119126
key={dao.id}
@@ -126,24 +133,45 @@ class DaosPage extends React.Component<IProps, IState> {
126133
<div className={css.wrapper}>
127134
<BreadcrumbsItem to="/daos/">All DAOs</BreadcrumbsItem>
128135

136+
<Link to={"/daos/create"} className={css.createDaoButton}>
137+
Create A DAO
138+
</Link>
139+
129140
<Sticky enabled top={50} innerZ={10000}>
130141
<div className={css.headerWrapper}>
131142
<div className={css.headerTitle + " clearfix"}>
132-
<h2 data-test-id="header-all-daos">All DAOs</h2>
143+
<h2 data-test-id="header-all-daos">Your DAOs</h2>
144+
</div>
145+
{yourDAOs.length ?
146+
<div className={css.searchBox}>
147+
<span>Search:</span>
148+
<input type="text" name="search" placeholder="DAO Name" onChange={this.onSearchChange} value={this.state.search} />
149+
</div>
150+
: ""}
151+
</div>
152+
</Sticky>
153+
{yourDAOs.length ?
154+
<div className={css.daoList}>
155+
{yourDaoNodes}
156+
</div>
157+
: <h2>Look for DAOs to join or follow below</h2>
158+
}
159+
160+
<Sticky enabled top={50} innerZ={10000}>
161+
<div className={css.headerWrapper}>
162+
<div className={css.headerTitle + " clearfix"}>
163+
<h2 data-test-id="header-all-daos">Other DAOs</h2>
133164
</div>
134165
<div className={css.searchBox}>
135166
<span>Search:</span>
136167
<input type="text" name="search" placeholder="DAO Name" onChange={this.onSearchChange} value={this.state.search} />
137168
</div>
138-
<Link to={"/daos/create"} className={css.createDaoButton}>
139-
Create A DAO
140-
</Link>
141169
</div>
142170
</Sticky>
143171
<div className={css.daoList}>
144-
{daoNodes ?
172+
{otherDaoNodes ?
145173
<InfiniteScroll
146-
dataLength={finalDAOList.length} // This is important field to render the next data
174+
dataLength={otherDaoNodes.length} // This is important field to render the next data
147175
next={fetchMore}
148176
hasMore
149177
loader=""
@@ -153,69 +181,65 @@ class DaosPage extends React.Component<IProps, IState> {
153181
</p>
154182
}
155183
>
156-
{daoNodes}
184+
{otherDaoNodes}
157185
</InfiniteScroll> : "None"}
158186
</div>
159187
</div>
160188
);
161189
}
162190
}
163191

192+
const createSubscriptionObservable = (props: IStateProps, data: SubscriptionData = null) => {
193+
const arc = getArc();
194+
const { currentAccountAddress, followingDAOs } = props;
195+
196+
// TODO: right now we don't handle a user following or being a member of more than 100 DAOs
197+
// it was too hard to figure out the UI with infinite scrolling in this case we would need a different UI
198+
199+
// Get list of DAO addresses the current user is a member of,
200+
// ignoring ones that they are following so we dont show those twice
201+
const memberDAOsquery = gql`
202+
query ReputationHolderSearch {
203+
reputationHolders(where: {
204+
address: "${currentAccountAddress}"
205+
${followingDAOs.length ? "dao_not_in: [" + followingDAOs.map(dao => "\"" + dao + "\"").join(",") + "]" : ""}
206+
},
207+
) {
208+
dao {
209+
...DAOFields
210+
}
211+
}
212+
}
213+
${DAOFieldsFragment}
214+
`;
215+
const memberOfDAOs = currentAccountAddress ? arc.getObservableList(memberDAOsquery, (r: any) => createDaoStateFromQuery(r.dao).dao, { subscribe: true }) : of([]);
216+
// eslint-disable-next-line @typescript-eslint/camelcase
217+
const followDAOs = followingDAOs.length ? arc.daos({ where: { id_in: followingDAOs }, orderBy: "name", orderDirection: "asc"}, { fetchAllData: true, subscribe: true }) : of([]);
218+
219+
return combineLatest(
220+
arc.daos({ orderBy: "name", orderDirection: "asc", first: PAGE_SIZE, skip: data ? data[0].length : 0}, { fetchAllData: true, subscribe: true }),
221+
followDAOs,
222+
memberOfDAOs
223+
);
224+
};
225+
164226
const SubscribedDaosPage = withSubscription({
165227
wrappedComponent: DaosPage,
166228
loadingComponent: <div className={css.wrapper}><Loading/></div>,
167229
errorComponent: (props) => <div>{ props.error.message }</div>,
168230

169231
// Don't ever update the subscription
170-
checkForUpdate: ["currentAccountAddress"],
232+
checkForUpdate: ["currentAccountAddress", "followingDAOs"],
171233

172234
// used for hacky pagination tracking
173235
pageSize: PAGE_SIZE,
174236

175-
createObservable: (props: IStateProps) => {
176-
const arc = getArc();
177-
178-
// Get list of DAO addresses the current user is a member of
179-
const memberDAOsquery = gql`
180-
query ReputationHolderSearch {
181-
reputationHolders(where: { address: "${props.currentAccountAddress}" }, first: ${PAGE_SIZE}, skip: 0) {
182-
dao {
183-
id
184-
}
185-
}
186-
}
187-
`;
188-
const memberOfDAOs = props.currentAccountAddress ? arc.getObservableList(memberDAOsquery, (r: any) => r.dao.id, { subscribe: true }) : of([]);
189-
190-
return combineLatest(
191-
arc.daos({ orderBy: "name", orderDirection: "asc", first: PAGE_SIZE, skip: 0}, { fetchAllData: true, subscribe: true }),
192-
memberOfDAOs
193-
);
194-
},
237+
createObservable: createSubscriptionObservable,
195238

196-
getFetchMoreObservable: (props: IStateProps, data: SubscriptionData) => {
197-
const arc = getArc();
198-
199-
// Get list of DAO addresses the current user is a member of
200-
const memberDAOsquery = gql`
201-
query ReputationHolderSearch {
202-
reputationHolders(where: { address: "${props.currentAccountAddress}" }, first: ${PAGE_SIZE}, skip: ${data[1].length}) {
203-
dao {
204-
id
205-
}
206-
}
207-
}
208-
`;
209-
const memberOfDAOs = props.currentAccountAddress ? arc.getObservableList(memberDAOsquery, (r: any) => r.dao.id, { subscribe: true }) : of([]);
210-
211-
return combineLatest(
212-
arc.daos({ orderBy: "name", orderDirection: "asc", first: PAGE_SIZE, skip: data[0].length}, { fetchAllData: true, subscribe: true }),
213-
memberOfDAOs
214-
);
215-
},
239+
getFetchMoreObservable: createSubscriptionObservable,
216240

217241
fetchMoreCombine: (prevData: SubscriptionData, newData: SubscriptionData) => {
218-
return [prevData[0].concat(newData[0]), prevData[1].concat(newData[1])] as SubscriptionData;
242+
return [prevData[0].concat(newData[0]), prevData[1], prevData[2]] as SubscriptionData;
219243
},
220244
});
221245

src/components/Feed/FeedPage.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Loading from "components/Shared/Loading";
44
import withSubscription, { ISubscriptionProps } from "components/Shared/withSubscription";
55
import gql from "graphql-tag";
66
import * as React from "react";
7+
import { BreadcrumbsItem } from "react-breadcrumbs-dynamic";
78
import * as InfiniteScroll from "react-infinite-scroll-component";
89
import { connect } from "react-redux";
910
import { IRootState } from "reducers";
@@ -53,6 +54,8 @@ class FeedPage extends React.Component<IProps, null> {
5354

5455
public renderEmptyFeed() {
5556
return <div className={css.emptyFeedBanner}>
57+
<BreadcrumbsItem to="/daos/">Feed</BreadcrumbsItem>
58+
5659
<img className={css.birds} src="/assets/images/birds.svg" />
5760
<h1>Looks like you&apos;re not following anything</h1>
5861
<h3>Follow DAOs, people and proposals to stay up to date with new proposals and their statuses</h3>
@@ -66,6 +69,8 @@ class FeedPage extends React.Component<IProps, null> {
6669

6770
if (!currentAccountAddress) {
6871
return <div className={css.emptyFeedBanner} data-test-id="not-logged-in-banner">
72+
<BreadcrumbsItem to="/daos/">Feed</BreadcrumbsItem>
73+
6974
<img src="/assets/images/unplugged.svg" />
7075
<h1>Hi there! Have we met before?</h1>
7176
<h3>Please Log In to see your personal feed</h3>
@@ -115,6 +120,8 @@ class FeedPage extends React.Component<IProps, null> {
115120

116121
return (
117122
<div className={css.feedContainer}>
123+
<BreadcrumbsItem to="/daos/">Feed</BreadcrumbsItem>
124+
118125
<InfiniteScroll
119126
dataLength={events.length} // This is important field to render the next data
120127
next={this.props.fetchMore}

0 commit comments

Comments
 (0)