Compare commits

..

No commits in common. "4b656d31e1f0c1e5581e35b038c52926a121ce11" and "57032a60f099aa0ca41890bedeba6d427bf4e73f" have entirely different histories.

13 changed files with 48 additions and 97 deletions

View File

@ -3,7 +3,7 @@ pipeline:
group: build
image: node
when:
path: "web/**/*"
path: "web/*"
environment:
- VITE_API_URL=https://api.treadl.com
- VITE_IMAGINARY_URL=https://images.treadl.com
@ -22,7 +22,7 @@ pipeline:
image: woodpeckerci/plugin-docker-buildx
secrets: [docker_username, docker_password]
when:
path: "api/**/*"
path: "api/*"
settings:
repo: wilw/treadl-api
dockerfile: api/Dockerfile
@ -32,7 +32,7 @@ pipeline:
image: alpine
secrets: [ LINODE_ACCESS_KEY, LINODE_SECRET_ACCESS_KEY, BUNNY_KEY ]
when:
path: "web/**/*"
path: "web/*"
commands:
- cd web
- apk update

View File

@ -7,10 +7,10 @@ from api import uploads, groups
def get_users(user):
db = database.get_db()
if 'root' not in user.get('roles', []): raise util.errors.Forbidden('Not allowed')
users = list(db.users.find({}, {'username': 1, 'avatar': 1, 'email': 1, 'createdAt': 1, 'lastSeenAt': 1, 'roles': 1, 'groups': 1}).sort('lastSeenAt', -1).limit(200))
users = list(db.users.find({}, {'username': 1, 'avatar': 1, 'email': 1, 'createdAt': 1, 'lastSeenAt': 1, 'roles': 1, 'groups': 1}).sort('lastSeenAt', -1))
group_ids = []
for u in users: group_ids += u.get('groups', [])
groups = list(db.groups.find({'_id': {'$in': group_ids}}, {'name': 1}))
groups = list(db.groups.find({'_id': {'$in': group_ids}}))
projects = list(db.projects.find({}, {'name': 1, 'path': 1, 'user': 1}))
for u in users:
if 'avatar' in u:

View File

@ -47,14 +47,12 @@ def update(user, username, data):
if db.users.find({'username': data['username'].lower()}).count():
raise util.errors.BadRequest('A user with this username already exists')
data['username'] = data['username'].lower()
if data.get('avatar') and len(data['avatar']) > 3: # Not a default avatar
if 'avatar' in data and len(data['avatar']) > 3: # Not a default avatar
def handle_cb(h):
db.users.update_one({'_id': user['_id']}, {'$set': {'avatarBlurHash': h}})
uploads.blur_image('users/' + str(user['_id']) + '/' + data['avatar'], handle_cb)
updater = util.build_updater(data, allowed_keys)
if updater:
if 'avatar' in updater.get('$unset', {}): # Also unset blurhash if removing avatar
updater['$unset']['avatarBlurHash'] = ''
db.users.update({'username': username}, updater)
return get(user, data.get('username', username))

Binary file not shown.

View File

@ -8,7 +8,6 @@
"@sentry/tracing": "^6.19.6",
"@vitejs/plugin-react": "^2.2.0",
"blurhash": "^1.1.3",
"boring-avatars": "^1.7.0",
"eventlistener": "^0.0.1",
"moment": "^2.22.2",
"pell": "^1.0.4",

View File

@ -20,8 +20,8 @@ const StyledNavBar = styled.div`
margin-top:5px;
}
.nav-links{
display: flex;
align-items: center;
vertical-align:middle;
margin-top:8px;
.ui.button{
margin-right: 8px;
}
@ -104,7 +104,7 @@ function NavBar() {
return (
<StyledNavBar>
<Container style={{display:'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<Container style={{display:'flex', justifyContent: 'space-between'}}>
<Link to="/"><img alt={`${utils.appName()} logo`} src={logoLight} className="logo" /></Link>
{isAuthenticated
? (
@ -185,13 +185,17 @@ function NavBar() {
<Button size='small' icon='help' basic inverted onClick={e => dispatch(actions.app.openHelpModal(true))}/>
</span>
<Dropdown direction="left" pointing="top right" icon={null} style={{marginLeft: 10, marginTop: 5}}
<Dropdown direction="left" pointing="top right" icon={null} style={{marginLeft: 10}}
trigger={<UserChip user={user} withoutLink avatarOnly />}
>
<Dropdown.Menu style={{ minWidth: '200px', paddingTop: 10 }}>
{user &&
<Dropdown.Header as={Link} to={`/${user.username}`}>
<UserChip user={user} />
<div style={{
display: 'inline-block', width: 30, height: 30, borderRadius: '50%', backgroundSize: 'cover', backgroundPosition: 'center center', backgroundImage: `url(${utils.avatarUrl(user)})`, verticalAlign: 'middle', marginRight: 8,
}}
/>
<span>{user.username}</span>
</Dropdown.Header>
}
{user?.isGoldSupporter && <Dropdown.Header><SupporterBadge type='gold' /></Dropdown.Header>}

View File

@ -1,18 +1,12 @@
import React from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import Avatar from 'boring-avatars';
import utils from '../../utils/utils.js';
import SupporterBadge from './SupporterBadge';
const StyledChip = styled(Link)`
position: relative;
display: flex;
align-items: center;
`;
const CustomAvatar = styled.div`
display: block;
const Avatar = styled.div`
display: inline-block;
position: relative;
width: ${props => props.compact ? 15 : 30}px;
height: ${props => props.compact ? 15 : 30}px;
@ -30,33 +24,21 @@ const Badge = styled.div`
`;
const Username = styled.span`
font-size: ${props => props.compact ? 10 : 12}px;
margin-left: 5px;
color: black;
`;
function UserChip({ user, compact, meta, withoutLink, avatarOnly, style }) {
if (!user) return null;
return (
<div style={{display: 'inline-block'}}>
<StyledChip to={withoutLink ? '#' : `/${user.username}`} style={style || {}}>
{user.avatar ?
<CustomAvatar compact={compact} user={user} avatarOnly={avatarOnly} />
:
<Avatar
size={compact ? 15 : 30}
name={user.username}
variant="beam"
colors={["#B2A4FF", "#FFB4B4", "#FFDEB4", "#FDF7C3"]}
/>
}
<Link to={withoutLink ? '#' : `/${user.username}`} style={style || {}}>
<Avatar compact={compact} user={user} avatarOnly={avatarOnly}>
{user.isGoldSupporter && <Badge><SupporterBadge compact type='gold' /></Badge>}
{user.isSilverSupporter && !user.isGoldSupporter && <Badge><SupporterBadge compact type='silver' /></Badge>}
</Avatar>
{!avatarOnly && <>
<Username compact={compact}>{user.username}</Username>
{meta && <span style={{color:'rgb(180,180,180)', fontSize: 10, marginLeft: 10}}>{meta}</span>}
</>}
</StyledChip>
</div>
</Link>
);
}

View File

@ -1,24 +1,14 @@
import React from 'react';
import { Divider, Container, Grid, Menu, Message } from 'semantic-ui-react';
import { Outlet, NavLink, Link } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { Divider, Container, Grid, Menu } from 'semantic-ui-react';
import { Outlet, NavLink } from 'react-router-dom';
function Settings() {
const { user } = useSelector(state => {
const user = state.users.users.filter(u => state.auth.currentUserId === u._id)[0];
return { user };
});
if (!user) return;
return (
<Container style={{ marginTop: '40px' }}>
<h2>Manage your account</h2>
<Divider hidden />
<Grid stackable>
<Grid.Column computer={4}>
<Message size='tiny'>You can change your profile information on your <Link to={`/${user.username}/edit`}>Profile page.</Link></Message>
<Menu fluid vertical tabular>
<Menu.Item as={NavLink} to="/settings/identity" name="identity" icon="user secret" />
<Menu.Item as={NavLink} to='/settings/notifications' content='Notifications' icon='envelope' />

View File

@ -21,13 +21,7 @@ function EditProfile() {
});
const updatePicture = (avatar) => {
api.users.update(profileUser.username, { avatar }, u => {
if (!avatar) { // Needed to ensure the avatar is immediately unset
u.avatar = null;
u.avatarUrl = null;
}
dispatch(actions.users.receive(u))
});
api.users.update(profileUser.username, { avatar }, u => dispatch(actions.users.receive(u)));
};
const updateProfile = () => {
@ -63,14 +57,16 @@ function EditProfile() {
trigger={<Button basic color="yellow" icon="image" content="Choose an image" />}
accept="image/*" onComplete={f => updatePicture(f.storedName)}
/>
{profileUser.avatar &&
<Button basic color="gray" icon="times" content="Remove image" onClick={() => {
utils.confirm(
'Really remove your image?',
'Your profile will be given a default avatar.'
).then(() => updatePicture(null), () => {});
}}/>
}
<h4>Or choose one of ours:</h4>
{utils.defaultAvatars().map(a => (
<img
alt="Default avatar" key={a.key} src={a.url}
style={{
width: 40, height: 40, margin: 4, cursor: 'pointer',
}}
onClick={e => updatePicture(a.key)}
/>
))}
<Divider hidden />
<Form>

View File

@ -3,7 +3,6 @@ import { Helmet } from 'react-helmet';
import { Loader, Icon, List, Container, Card, Grid, Message } from 'semantic-ui-react';
import { Link, Outlet, useParams } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import Avatar from 'boring-avatars';
import moment from 'moment';
import utils from '../../../utils/utils';
import actions from '../../../actions';
@ -58,21 +57,12 @@ function Profile() {
<Card raised color="yellow">
<Card.Content>
<div style={{textAlign: 'center'}}>
{profileUser.avatar ?
<BlurrableImage
src={utils.cropUrl(utils.avatarUrl(profileUser), 200, 200)}
blurHash={profileUser.avatarBlurHash}
width={200} height={200}
style={{ borderRadius: '50%' }}
/>
:
<Avatar
size={200}
name={profileUser.username}
variant="beam"
colors={["#B2A4FF", "#FFB4B4", "#FFDEB4", "#FDF7C3"]}
/>
}
</div>
<Card.Header>{profileUser.username}</Card.Header>
<Card.Meta>

View File

@ -42,8 +42,8 @@ const utils = {
return window.location.protocol + '//' + window.location.host + path;
},
avatarUrl(user) {
const avatar = user?.avatar;
if (avatar?.length < 3) {
const avatar = (user && user.avatar) || '9';
if (avatar.length < 3) {
const a = utils.defaultAvatars().filter(a => a.key === avatar)[0];
return a && a.url;
}

View File

@ -1071,13 +1071,6 @@ __metadata:
languageName: node
linkType: hard
"boring-avatars@npm:^1.7.0":
version: 1.7.0
resolution: "boring-avatars@npm:1.7.0"
checksum: 5f39a7b3a011ba8651c6bec22c453b42c58c0fc347b3dce5cfea7d4de4208b1296f1fb4efc66ebb598f805124a07c5b8e3b06766f5211e1deb545bbf889230b0
languageName: node
linkType: hard
"brace-expansion@npm:^1.1.7":
version: 1.1.11
resolution: "brace-expansion@npm:1.1.11"
@ -3555,7 +3548,6 @@ __metadata:
"@sentry/tracing": ^6.19.6
"@vitejs/plugin-react": ^2.2.0
blurhash: ^1.1.3
boring-avatars: ^1.7.0
eventlistener: ^0.0.1
moment: ^2.22.2
pell: ^1.0.4