Compare commits
No commits in common. "4b656d31e1f0c1e5581e35b038c52926a121ce11" and "57032a60f099aa0ca41890bedeba6d427bf4e73f" have entirely different histories.
4b656d31e1
...
57032a60f0
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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.
Binary file not shown.
@ -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",
|
||||
|
@ -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>}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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' />
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user