Compare commits

..

No commits in common. "9e9491e064f20916fb075b9f67707126b986d176" and "447f76e80742ea70fa6a7c651fc214bb2f508dd6" have entirely different histories.

11 changed files with 87 additions and 158 deletions

View File

@ -42,15 +42,15 @@ def users(user, params):
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar'])) u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar']))
return {'users': users} return {'users': users}
def discover(user, count = 3): def discover(user):
if not user: raise util.errors.Forbidden('You need to be logged in')
db = database.get_db() db = database.get_db()
projects = [] projects = []
users = [] users = []
count = 3
all_projects_query = {'name': {'$not': re.compile('my new project', re.IGNORECASE)}, 'visibility': 'public'} all_projects = list(db.projects.find({'name': {'$not': re.compile('my new project', re.IGNORECASE)}, 'visibility': 'public', 'user': {'$ne': user['_id']}}, {'name': 1, 'path': 1, 'user': 1}))
if user and user.get('_id'):
all_projects_query['user'] = {'$ne': user['_id']}
all_projects = list(db.projects.find(all_projects_query, {'name': 1, 'path': 1, 'user': 1}))
random.shuffle(all_projects) random.shuffle(all_projects)
for p in all_projects: for p in all_projects:
if db.objects.find_one({'project': p['_id'], 'name': {'$ne': 'Untitled pattern'}}): if db.objects.find_one({'project': p['_id'], 'name': {'$ne': 'Untitled pattern'}}):
@ -60,10 +60,7 @@ def discover(user, count = 3):
if len(projects) >= count: break if len(projects) >= count: break
interest_fields = ['bio', 'avatar', 'website', 'facebook', 'twitter', 'instagram', 'location'] interest_fields = ['bio', 'avatar', 'website', 'facebook', 'twitter', 'instagram', 'location']
all_users_query = {'$or': list(map(lambda f: {f: {'$exists': True}}, interest_fields))} all_users = list(db.users.find({'_id': {'$ne': user['_id']}, '$or': list(map(lambda f: {f: {'$exists': True}}, interest_fields))}, {'username': 1, 'avatar': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1}))
if user and user.get('_id'):
all_users_query['_id'] = {'$ne': user['_id']}
all_users = list(db.users.find(all_users_query, {'username': 1, 'avatar': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1}))
random.shuffle(all_users) random.shuffle(all_users)
for u in all_users: for u in all_users:
if 'avatar' in u: if 'avatar' in u:
@ -76,17 +73,16 @@ def discover(user, count = 3):
'highlightUsers': users, 'highlightUsers': users,
} }
def explore(page = 1): def explore():
db = database.get_db() db = database.get_db()
per_page = 10
project_map = {} project_map = {}
user_map = {} user_map = {}
all_public_projects = list(db.projects.find({'name': {'$not': re.compile('my new project', re.IGNORECASE)}, 'visibility': 'public'}, {'name': 1, 'path': 1, 'user': 1})) all_public_projects = list(db.projects.find({'name': {'$not': re.compile('my new project', re.IGNORECASE)}, 'visibility': 'public'}, {'name': 1, 'path': 1, 'user': 1}))
all_public_project_ids = list(map(lambda p: p['_id'], all_public_projects)) all_public_project_ids = list(map(lambda p: p['_id'], all_public_projects))
for project in all_public_projects: for project in all_public_projects:
project_map[project['_id']] = project project_map[project['_id']] = project
objects = list(db.objects.find({'project': {'$in': all_public_project_ids}, 'name': {'$not': re.compile('untitled pattern', re.IGNORECASE)}, 'preview': {'$exists': True}}, {'project': 1, 'name': 1, 'createdAt': 1, 'preview': 1}).sort('createdAt', pymongo.DESCENDING).skip((page - 1) * per_page).limit(per_page)) objects = list(db.objects.find({'project': {'$in': all_public_project_ids}, 'name': {'$not': re.compile('untitled pattern', re.IGNORECASE)}, 'preview': {'$exists': True}}, {'project': 1, 'name': 1, 'createdAt': 1, 'preview': 1}).sort('createdAt', pymongo.DESCENDING).limit(20))
for object in objects: for object in objects:
object['projectObject'] = project_map.get(object['project']) object['projectObject'] = project_map.get(object['project'])
authors = list(db.users.find({'_id': {'$in': list(map(lambda o: o.get('projectObject', {}).get('user'), objects))}}, {'username': 1, 'avatar': 1})) authors = list(db.users.find({'_id': {'$in': list(map(lambda o: o.get('projectObject', {}).get('user'), objects))}}, {'username': 1, 'avatar': 1}))
@ -96,6 +92,5 @@ def explore(page = 1):
user_map[a['_id']] = a user_map[a['_id']] = a
for object in objects: for object in objects:
object['userObject'] = user_map.get(object.get('projectObject', {}).get('user')) object['userObject'] = user_map.get(object.get('projectObject', {}).get('user'))
return {'objects': objects} return {'objects': objects}

View File

@ -265,15 +265,11 @@ def search_users():
@app.route('/search/discover', methods=['GET']) @app.route('/search/discover', methods=['GET'])
def search_discover(): def search_discover():
count = request.args.get('count', 3) return util.jsonify(search.discover(util.get_user(required=True)))
if count: count = int(count)
return util.jsonify(search.discover(util.get_user(required=False), count=count))
@app.route('/search/explore', methods=['GET']) @app.route('/search/explore', methods=['GET'])
def search_explore(): def search_explore():
page = request.args.get('page', 1) return util.jsonify(search.explore())
if page: page = int(page)
return util.jsonify(search.explore(page=page))
# INVITATIONS # INVITATIONS

View File

@ -2,7 +2,6 @@ export default {
RECEIVE_OBJECTS: 'RECEIVE_OBJECTS', RECEIVE_OBJECTS: 'RECEIVE_OBJECTS',
RECEIVE_OBJECT: 'RECEIVE_OBJECT', RECEIVE_OBJECT: 'RECEIVE_OBJECT',
RECEIVE_EXPLORE_OBJECTS: 'RECEIVE_EXPLORE_OBJECTS',
CREATE_OBJECT: 'CREATE_OBJECT', CREATE_OBJECT: 'CREATE_OBJECT',
UPDATE_OBJECT: 'UPDATE_OBJECT', UPDATE_OBJECT: 'UPDATE_OBJECT',
DELETE_OBJECT: 'DELETE_OBJECT', DELETE_OBJECT: 'DELETE_OBJECT',
@ -18,10 +17,6 @@ export default {
receive(object) { receive(object) {
return { type: this.RECEIVE_OBJECT, object }; return { type: this.RECEIVE_OBJECT, object };
}, },
receiveExplore(objects) {
return { type: this.RECEIVE_EXPLORE_OBJECTS, objects };
},
create(object) { create(object) {
return { type: this.CREATE_OBJECT, object }; return { type: this.CREATE_OBJECT, object };

View File

@ -8,7 +8,6 @@ export default {
REQUEST_USERS: 'REQUEST_USERS', REQUEST_USERS: 'REQUEST_USERS',
REQUEST_FAILED: 'REQUEST_FAILED', REQUEST_FAILED: 'REQUEST_FAILED',
RECEIVE_USER: 'RECEIVE_USERS', RECEIVE_USER: 'RECEIVE_USERS',
RECEIVE_EXPLORE: 'RECEIVE_EXPLORE',
UPDATE_USER: 'UPDATE_USER', UPDATE_USER: 'UPDATE_USER',
UPDATE_USERNAME: 'UPDATE_USERNAME', UPDATE_USERNAME: 'UPDATE_USERNAME',
JOIN_GROUP: 'JOIN_GROUP', JOIN_GROUP: 'JOIN_GROUP',
@ -34,10 +33,6 @@ export default {
receive(user) { receive(user) {
return { type: this.RECEIVE_USER, user }; return { type: this.RECEIVE_USER, user };
}, },
receiveExplore(users) {
return { type: this.RECEIVE_EXPLORE, users };
},
update(id, data) { update(id, data) {
return { type: this.UPDATE_USER, id, data }; return { type: this.UPDATE_USER, id, data };

View File

@ -7,10 +7,10 @@ export const search = {
users(username, success, fail) { users(username, success, fail) {
api.authenticatedRequest('GET', `/search/users?username=${username}`, null, data => success && success(data.users), fail); api.authenticatedRequest('GET', `/search/users?username=${username}`, null, data => success && success(data.users), fail);
}, },
discover(count, success, fail) { discover(success, fail) {
api.authenticatedRequest('GET', `/search/discover?count=${count || 3}`, null, data => success && success(data), fail); api.authenticatedRequest('GET', `/search/discover`, null, data => success && success(data), fail);
}, },
explore(page, success, fail) { explore(success, fail) {
api.unauthenticatedRequest('GET', `/search/explore?page=${page || 1}`, null, data => success && success(data), fail); api.unauthenticatedRequest('GET', `/search/explore`, null, data => success && success(data), fail);
}, },
}; };

View File

@ -78,12 +78,6 @@ function App() {
useEffect(() => { useEffect(() => {
api.auth.autoLogin(token => dispatch(actions.auth.receiveLogin(token))); api.auth.autoLogin(token => dispatch(actions.auth.receiveLogin(token)));
}, [dispatch]); }, [dispatch]);
useEffect(() => {
api.search.explore(1, data => { // Page is always 1 on app-load
dispatch(actions.objects.receiveExplore(data.objects));
});
}, []);
useEffect(() => { useEffect(() => {
if (!loggedInUserId) return; if (!loggedInUserId) return;

View File

@ -1,53 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Card, List } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import UserChip from './UserChip';
import api from '../../api';
import utils from '../../utils/utils.js';
export default function ExploreCard({ count }) {
const [highlightProjects, setHighlightProjects] = useState([]);
const [highlightUsers, setHighlightUsers] = useState([]);
useEffect(() => {
api.search.discover(count || 3, ({ highlightProjects, highlightUsers }) => {
setHighlightProjects(highlightProjects);
setHighlightUsers(highlightUsers);
});
}, []);
if ((highlightProjects?.length === 0 || highlightUsers?.length === 0)) return null;
return (
<Card fluid>
<Card.Content>
{highlightProjects?.length > 0 && <>
<h4>Discover a project</h4>
<List relaxed>
{highlightProjects.map(p =>
<List.Item key={p._id}>
<List.Icon name='book' size='large' verticalAlign='middle' />
<List.Content>
<List.Header className='umami--click--discover-project' as={Link} to={`/${p.fullName}`}>{p.name}</List.Header>
</List.Content>
</List.Item>
)}
</List>
</>}
{highlightUsers?.length > 0 && <>
<h4>Find others on {utils.appName()}</h4>
<List relaxed>
{highlightUsers.map(u =>
<List.Item key={u._id}>
<List.Content>
<UserChip user={u} className='umami--click--discover-user'/>
</List.Content>
</List.Item>
)}
</List>
</>}
</Card.Content>
</Card>
);
}

View File

@ -12,10 +12,11 @@ import UserChip from '../includes/UserChip';
import HelpLink from '../includes/HelpLink'; import HelpLink from '../includes/HelpLink';
import ProjectCard from '../includes/ProjectCard'; import ProjectCard from '../includes/ProjectCard';
import Tour from '../includes/Tour'; import Tour from '../includes/Tour';
import DiscoverCard from '../includes/DiscoverCard';
function Home() { function Home() {
const [runJoyride, setRunJoyride] = useState(false); const [runJoyride, setRunJoyride] = useState(false);
const [highlightProjects, setHighlightProjects] = useState([]);
const [highlightUsers, setHighlightUsers] = useState([]);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { user, projects, groups, invitations, loadingProjects } = useSelector(state => { const { user, projects, groups, invitations, loadingProjects } = useSelector(state => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0]; const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
@ -29,6 +30,10 @@ function Home() {
api.invitations.get(({ invitations, sentInvitations}) => { api.invitations.get(({ invitations, sentInvitations}) => {
dispatch(actions.invitations.receiveInvitations(invitations.concat(sentInvitations))); dispatch(actions.invitations.receiveInvitations(invitations.concat(sentInvitations)));
}); });
api.search.discover(({ highlightProjects, highlightUsers }) => {
setHighlightProjects(highlightProjects);
setHighlightUsers(highlightUsers);
});
}, [dispatch]); }, [dispatch]);
useEffect(() => { useEffect(() => {
api.users.getMyProjects(p => dispatch(actions.projects.receiveProjects(p))); api.users.getMyProjects(p => dispatch(actions.projects.receiveProjects(p)));
@ -87,7 +92,38 @@ function Home() {
<h2><span role="img" aria-label="wave">👋</span> {greeting}{user && <span>, {user.username}</span>}</h2> <h2><span role="img" aria-label="wave">👋</span> {greeting}{user && <span>, {user.username}</span>}</h2>
<DiscoverCard count={3} /> {(highlightProjects?.length > 0 || highlightUsers?.length > 0) &&
<Card fluid>
<Card.Content>
{highlightProjects?.length > 0 && <>
<h4>Discover public projects</h4>
<List relaxed>
{highlightProjects.map(p =>
<List.Item key={p._id}>
<List.Icon name='book' size='large' verticalAlign='middle' />
<List.Content>
<List.Header className='umami--click--discover-project' as={Link} to={p.fullName}>{p.name}</List.Header>
</List.Content>
</List.Item>
)}
</List>
</>}
{highlightUsers?.length > 0 && <>
<h4>Find others on {utils.appName()}</h4>
<List relaxed>
{highlightUsers.map(u =>
<List.Item key={u._id}>
<List.Content>
<UserChip user={u} className='umami--click--discover-user'/>
</List.Content>
</List.Item>
)}
</List>
</>}
</Card.Content>
</Card>
}
{(groups && groups.length) ? {(groups && groups.length) ?
<Card fluid className='joyride-groups' style={{opacity: 0.8}}> <Card fluid className='joyride-groups' style={{opacity: 0.8}}>

View File

@ -1,68 +1,47 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Container, Card, Grid, Button } from 'semantic-ui-react'; import { Container, Card } from 'semantic-ui-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import actions from '../../../actions';
import api from '../../../api'; import api from '../../../api';
import utils from '../../../utils/utils.js'; import utils from '../../../utils/utils.js';
import UserChip from '../../includes/UserChip'; import UserChip from '../../includes/UserChip';
import DiscoverCard from '../../includes/DiscoverCard';
import DraftPreview from '../projects/objects/DraftPreview'; import DraftPreview from '../projects/objects/DraftPreview';
export default function Explore() { export default function Explore() {
const [objects, setObjects] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const { objects, page } = useSelector(state => {
return { objects: state.objects.exploreObjects, page: state.objects.explorePage };
});
useEffect(() => { useEffect(() => {
if (page < 2) loadMoreExplore(); api.search.explore(data => {
}, []); setObjects(data.objects);
function loadMoreExplore() {
setLoading(true);
api.search.explore(page + 1, data => {
dispatch(actions.objects.receiveExplore(data.objects));
setLoading(false);
}); });
} }, [])
return ( return (
<Container style={{ marginTop: '40px' }}> <Container style={{ marginTop: '40px' }}>
<Grid stackable> <h1>Explore {utils.appName()}</h1>
<Grid.Column computer={5} tablet={8}>
<DiscoverCard count={7} /> <Card.Group stackable doubling itemsPerRow={4}>
</Grid.Column>
<Grid.Column computer={11} tablet={8}> {objects?.filter(o => o.projectObject && o.userObject).map(object =>
<h1>Recent patterns on {utils.appName()}</h1> <Card raised key={object._id} style={{ cursor: 'pointer' }} as={Link} to={`/${object.userObject?.username}/${object.projectObject?.path}/${object._id}`}>
<div style={{ height: 200, backgroundImage: `url(${object.preview})`, backgroundSize: 'cover', backgroundPosition: 'top right', position: 'relative' }}>
<Card.Group stackable doubling itemsPerRow={3} style={{marginTop: 30}}> {object.userObject &&
{objects?.filter(o => o.projectObject && o.userObject).map(object => <div style={{position: 'absolute', top: 5, left: 5, padding: 3, background: 'rgba(250,250,250,0.5)', borderRadius: 5}}>
<Card raised key={object._id} style={{ cursor: 'pointer' }} as={Link} to={`/${object.userObject?.username}/${object.projectObject?.path}/${object._id}`}> <UserChip user={object.userObject} />
<div style={{ height: 200, backgroundImage: `url(${object.preview})`, backgroundSize: 'cover', backgroundPosition: 'top right', position: 'relative' }}> </div>
{object.userObject && }
<div style={{position: 'absolute', top: 5, left: 5, padding: 3, background: 'rgba(250,250,250,0.5)', borderRadius: 5}}>
<UserChip user={object.userObject} />
</div>
}
</div>
<Card.Content>
<p style={{ wordBreak: 'break-all' }}>{object.name}</p>
{object.projectObject?.path &&
<p style={{fontSize: 11, color: 'black'}}>{object.projectObject.path}</p>
}
</Card.Content>
</Card>
)}
</Card.Group>
<div style={{display: 'flex', justifyContent: 'center', marginTop: 30}}>
<Button loading={loading} onClick={loadMoreExplore}>Load more</Button>
</div> </div>
</Grid.Column> <Card.Content>
</Grid> <p style={{ wordBreak: 'break-all' }}>{object.name}</p>
{object.projectObject?.path &&
<p style={{fontSize: 11, color: 'black'}}>{object.projectObject.path}</p>
}
</Card.Content>
</Card>
)}
</Card.Group>
</Container> </Container>
) )
} }

View File

@ -119,7 +119,7 @@ function ObjectViewer() {
<div style={{ display: 'flex', justifyContent: 'end' }}> <div style={{ display: 'flex', justifyContent: 'end' }}>
{object.type === 'pattern' && (project.user === (user && user._id) || project.openSource || object.preview) && <> {object.type === 'pattern' && (project.user === (user && user._id) || project.openSource || object.preview) && <>
<Dropdown direction='left' icon={null} trigger={<Button size='small' secondary icon='download' content='Download pattern' loading={downloading} disabled={downloading}/>}> <Dropdown icon={null} trigger={<Button size='small' secondary icon='download' content='Download pattern' loading={downloading} disabled={downloading}/>}>
<Dropdown.Menu> <Dropdown.Menu>
{object.preview && {object.preview &&
<Dropdown.Item onClick={e => downloadDrawdownImage(object)} content='Download drawdown as an image' icon='file outline' /> <Dropdown.Item onClick={e => downloadDrawdownImage(object)} content='Download drawdown as an image' icon='file outline' />
@ -136,14 +136,12 @@ function ObjectViewer() {
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
{user && <Dropdown icon={null} trigger={<Button size="small" icon="copy" secondary content="Copy to.." />}>
<Dropdown icon={null} trigger={<Button size="small" icon="copy" secondary content="Copy to.." />}> <Dropdown.Menu>
<Dropdown.Menu> <Dropdown.Header>Select a project to copy this pattern to</Dropdown.Header>
<Dropdown.Header>Select a project to copy this pattern to</Dropdown.Header> {myProjects?.map(myProject => <Dropdown.Item content={myProject.name} onClick={e => copyPattern(myProject)} />)}
{myProjects?.map(myProject => <Dropdown.Item content={myProject.name} onClick={e => copyPattern(myProject)} />)} </Dropdown.Menu>
</Dropdown.Menu> </Dropdown>
</Dropdown>
}
</>} </>}
{utils.canEditProject(user, project) && {utils.canEditProject(user, project) &&

View File

@ -3,8 +3,6 @@ import actions from '../actions';
const initialState = { const initialState = {
loading: false, loading: false,
objects: [], objects: [],
exploreObjects: [],
explorePage: 1,
comments: [], comments: [],
selected: null, selected: null,
editor: { tool: 'straight', colour: 'orange', view: 'interlacement' }, editor: { tool: 'straight', colour: 'orange', view: 'interlacement' },
@ -38,10 +36,6 @@ function objects(state = initialState, action) {
}); });
if (!found) objects.push(action.object); if (!found) objects.push(action.object);
return Object.assign({}, state, { loading: false, objects }); return Object.assign({}, state, { loading: false, objects });
case actions.objects.RECEIVE_EXPLORE_OBJECTS:
const newObjects = Object.assign([], state.exploreObjects);
action.objects?.forEach(o => newObjects.push(o));
return Object.assign({}, state, { exploreObjects: newObjects, explorePage: state.explorePage + 1 });
case actions.objects.CREATE_OBJECT: case actions.objects.CREATE_OBJECT:
const objectList = state.objects; const objectList = state.objects;
objectList.push(action.object); objectList.push(action.object);