Browse Source

Adding user avatars / icons. Requires pictshare.

- Fixes #188
unoptim
Dessalines 2 years ago
parent
commit
9358eff926
  1. 2
      .eslintignore
  2. 26
      fuse.js
  3. 18
      src/components/comment-node.tsx
  4. 14
      src/components/main.tsx
  5. 16
      src/components/navbar.tsx
  6. 11
      src/components/post-listing.tsx
  7. 15
      src/components/search.tsx
  8. 12
      src/components/sidebar.tsx
  9. 82
      src/components/user.tsx
  10. 7
      src/interfaces.ts
  11. 1
      src/translations/en.ts
  12. 7
      src/utils.ts
  13. 20
      translation_report.ts

2
.eslintignore

@ -0,0 +1,2 @@
fuse.js
translation_report.ts

26
fuse.js

@ -1,11 +1,11 @@
const {
import {
FuseBox,
Sparky,
EnvPlugin,
CSSPlugin,
WebIndexPlugin,
QuantumPlugin
} = require('fuse-box');
QuantumPlugin,
} from 'fuse-box';
// const transformInferno = require('../../dist').default
const transformInferno = require('ts-transform-inferno').default;
const transformClasscat = require('ts-transform-classcat').default;
@ -25,22 +25,22 @@ Sparky.task('config', _ => {
before: [transformClasscat(), transformInferno()],
},
alias: {
'locale': 'moment/locale'
},
locale: 'moment/locale',
},
plugins: [
EnvPlugin({ NODE_ENV: isProduction ? 'production' : 'development' }),
CSSPlugin(),
WebIndexPlugin({
title: 'Inferno Typescript FuseBox Example',
template: 'src/index.html',
path: isProduction ? "/static" : "/"
path: isProduction ? '/static' : '/',
}),
isProduction &&
QuantumPlugin({
bakeApiIntoBundle: 'app',
treeshake: true,
uglify: true,
}),
QuantumPlugin({
bakeApiIntoBundle: 'app',
treeshake: true,
uglify: true,
}),
],
});
app = fuse.bundle('app').instructions('>index.tsx');
@ -48,7 +48,9 @@ Sparky.task('config', _ => {
// Sparky.task('version', _ => setVersion());
Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
Sparky.task('env', _ => (isProduction = true));
Sparky.task('copy-assets', () => Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static'));
Sparky.task('copy-assets', () =>
Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static')
);
Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
fuse.dev();
app.hmr().watch();

18
src/components/comment-node.tsx

@ -17,7 +17,13 @@ import {
BanType,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { mdToHtml, getUnixTime, canMod, isMod } from '../utils';
import {
mdToHtml,
getUnixTime,
canMod,
isMod,
pictshareAvatarThumbnail,
} from '../utils';
import * as moment from 'moment';
import { MomentTime } from './moment-time';
import { CommentForm } from './comment-form';
@ -128,7 +134,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className="text-info"
to={`/u/${node.comment.creator_name}`}
>
{node.comment.creator_name}
{node.comment.creator_avatar && (
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(node.comment.creator_avatar)}
class="rounded-circle mr-1"
/>
)}
<span>{node.comment.creator_name}</span>
</Link>
</li>
{this.isMod && (

14
src/components/main.tsx

@ -31,6 +31,7 @@ import {
routeSortTypeToEnum,
routeListingTypeToEnum,
postRefetchSeconds,
pictshareAvatarThumbnail,
} from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
@ -65,6 +66,9 @@ export class Main extends Component<any, MainState> {
number_of_posts: null,
number_of_comments: null,
number_of_communities: null,
enable_downvotes: null,
open_registration: null,
enable_nsfw: null,
},
admins: [],
banned: [],
@ -341,7 +345,15 @@ export class Main extends Component<any, MainState> {
{this.state.site.admins.map(admin => (
<li class="list-inline-item">
<Link class="text-info" to={`/u/${admin.name}`}>
{admin.name}
{admin.avatar && (
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(admin.avatar)}
class="rounded-circle mr-1"
/>
)}
<span>{admin.name}</span>
</Link>
</li>
))}

16
src/components/navbar.tsx

@ -13,7 +13,7 @@ import {
GetSiteResponse,
Comment,
} from '../interfaces';
import { msgOp } from '../utils';
import { msgOp, pictshareAvatarThumbnail } from '../utils';
import { version } from '../version';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
@ -151,7 +151,19 @@ export class Navbar extends Component<any, NavbarState> {
class="nav-link"
to={`/u/${UserService.Instance.user.username}`}
>
{UserService.Instance.user.username}
<span>
{UserService.Instance.user.avatar && (
<img
src={pictshareAvatarThumbnail(
UserService.Instance.user.avatar
)}
height="32"
width="32"
class="rounded-circle mr-2"
/>
)}
{UserService.Instance.user.username}
</span>
</Link>
</li>
</>

11
src/components/post-listing.tsx

@ -25,6 +25,7 @@ import {
isImage,
isVideo,
getUnixTime,
pictshareAvatarThumbnail,
} from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
@ -248,7 +249,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<li className="list-inline-item">
<span>{i18n.t('by')} </span>
<Link className="text-info" to={`/u/${post.creator_name}`}>
{post.creator_name}
{post.creator_avatar && (
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(post.creator_avatar)}
class="rounded-circle mr-1"
/>
)}
<span>{post.creator_name}</span>
</Link>
{this.isMod && (
<span className="mx-1 badge badge-light">

15
src/components/search.tsx

@ -19,6 +19,7 @@ import {
fetchLimit,
routeSearchTypeToEnum,
routeSortTypeToEnum,
pictshareAvatarThumbnail,
} from '../utils';
import { PostListing } from './post-listing';
import { SortSelect } from './sort-select';
@ -286,7 +287,19 @@ export class Search extends Component<any, SearchState> {
<Link
className="text-info"
to={`/u/${(i.data as UserView).name}`}
>{`/u/${(i.data as UserView).name}`}</Link>
>
{(i.data as UserView).avatar && (
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(
(i.data as UserView).avatar
)}
class="rounded-circle mr-1"
/>
)}
<span>{`/u/${(i.data as UserView).name}`}</span>
</Link>
</span>
<span>{` - ${
(i.data as UserView).comment_score

12
src/components/sidebar.tsx

@ -8,7 +8,7 @@ import {
UserView,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { mdToHtml, getUnixTime } from '../utils';
import { mdToHtml, getUnixTime, pictshareAvatarThumbnail } from '../utils';
import { CommunityForm } from './community-form';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
@ -194,7 +194,15 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{this.props.moderators.map(mod => (
<li class="list-inline-item">
<Link class="text-info" to={`/u/${mod.user_name}`}>
{mod.user_name}
{mod.avatar && (
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(mod.avatar)}
class="rounded-circle mr-1"
/>
)}
<span>{mod.user_name}</span>
</Link>
</li>
))}

82
src/components/user.tsx

@ -58,6 +58,7 @@ interface UserState {
sort: SortType;
page: number;
loading: boolean;
avatarLoading: boolean;
userSettingsForm: UserSettingsForm;
userSettingsLoading: boolean;
deleteAccountLoading: boolean;
@ -78,6 +79,7 @@ export class User extends Component<any, UserState> {
number_of_comments: null,
comment_score: null,
banned: null,
avatar: null,
},
user_id: null,
username: null,
@ -87,6 +89,7 @@ export class User extends Component<any, UserState> {
posts: [],
admins: [],
loading: true,
avatarLoading: false,
view: this.getViewFromProps(this.props),
sort: this.getSortTypeFromProps(this.props),
page: this.getPageFromProps(this.props),
@ -96,6 +99,7 @@ export class User extends Component<any, UserState> {
default_sort_type: null,
default_listing_type: null,
lang: null,
avatar: null,
auth: null,
},
userSettingsLoading: null,
@ -203,7 +207,17 @@ export class User extends Component<any, UserState> {
) : (
<div class="row">
<div class="col-12 col-md-8">
<h5>/u/{this.state.user.name}</h5>
<h5>
{this.state.user.avatar && (
<img
height="80"
width="80"
src={this.state.user.avatar}
class="rounded-circle mr-2"
/>
)}
<span>/u/{this.state.user.name}</span>
</h5>
{this.selects()}
{this.state.view == View.Overview && this.overview()}
{this.state.view == View.Comments && this.comments()}
@ -422,6 +436,39 @@ export class User extends Component<any, UserState> {
<T i18nKey="settings">#</T>
</h5>
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
<div class="form-group">
<div class="col-12">
<label>
<T i18nKey="avatar">#</T>
</label>
<form class="d-inline">
<label
htmlFor="file-upload"
class="pointer ml-4 text-muted small font-weight-bold"
>
<img
height="80"
width="80"
src={
this.state.userSettingsForm.avatar
? this.state.userSettingsForm.avatar
: 'https://via.placeholder.com/300/000?text=Avatar'
}
class="rounded-circle"
/>
</label>
<input
id="file-upload"
type="file"
accept="image/*,video/*"
name="file"
class="d-none"
disabled={!UserService.Instance.user}
onChange={linkEvent(this, this.handleImageUpload)}
/>
</form>
</div>
</div>
<div class="form-group">
<div class="col-12">
<label>
@ -739,6 +786,38 @@ export class User extends Component<any, UserState> {
this.setState(this.state);
}
handleImageUpload(i: User, event: any) {
event.preventDefault();
let file = event.target.files[0];
const imageUploadUrl = `/pictshare/api/upload.php`;
const formData = new FormData();
formData.append('file', file);
i.state.avatarLoading = true;
i.setState(i.state);
fetch(imageUploadUrl, {
method: 'POST',
body: formData,
})
.then(res => res.json())
.then(res => {
let url = `${window.location.origin}/pictshare/${res.url}`;
if (res.filetype == 'mp4') {
url += '/raw';
}
i.state.userSettingsForm.avatar = url;
console.log(url);
i.state.avatarLoading = false;
i.setState(i.state);
})
.catch(error => {
i.state.avatarLoading = false;
i.setState(i.state);
alert(error);
});
}
handleUserSettingsSubmit(i: User, event: any) {
event.preventDefault();
i.state.userSettingsLoading = true;
@ -802,6 +881,7 @@ export class User extends Component<any, UserState> {
this.state.userSettingsForm.default_listing_type =
UserService.Instance.user.default_listing_type;
this.state.userSettingsForm.lang = UserService.Instance.user.lang;
this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
}
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
window.scrollTo(0, 0);

7
src/interfaces.ts

@ -80,11 +80,13 @@ export interface User {
default_sort_type: SortType;
default_listing_type: ListingType;
lang: string;
avatar?: string;
}
export interface UserView {
id: number;
name: string;
avatar?: string;
fedi_name: string;
published: string;
number_of_posts: number;
@ -98,6 +100,7 @@ export interface CommunityUser {
id: number;
user_id: number;
user_name: string;
avatar?: string;
community_id: number;
community_name: string;
published: string;
@ -116,6 +119,7 @@ export interface Community {
published: string;
updated?: string;
creator_name: string;
creator_avatar?: string;
category_name: string;
number_of_subscribers: number;
number_of_posts: number;
@ -141,6 +145,7 @@ export interface Post {
published: string;
updated?: string;
creator_name: string;
creator_avatar?: string;
community_name: string;
community_removed: boolean;
community_deleted: boolean;
@ -172,6 +177,7 @@ export interface Comment {
banned: boolean;
banned_from_community: boolean;
creator_name: string;
creator_avatar?: string;
score: number;
upvotes: number;
downvotes: number;
@ -474,6 +480,7 @@ export interface UserSettingsForm {
default_sort_type: SortType;
default_listing_type: ListingType;
lang: string;
avatar?: string;
auth: string;
}

1
src/translations/en.ts

@ -28,6 +28,7 @@ export const en = {
cancel: 'Cancel',
preview: 'Preview',
upload_image: 'upload image',
avatar: 'Avatar',
formatting_help: 'formatting help',
view_source: 'view source',
unlock: 'unlock',

7
src/utils.ts

@ -338,3 +338,10 @@ export function objectFlip(obj: any) {
});
return ret;
}
export function pictshareAvatarThumbnail(src: string): string {
// sample url: http://localhost:8535/pictshare/gs7xuu.jpg
let split = src.split('pictshare');
let out = `${split[0]}pictshare/96x96${split[1]}`;
return out;
}

20
translation_report.ts

@ -10,15 +10,15 @@ import { nl } from './src/translations/nl';
import { it } from './src/translations/it';
let files = [
{t: de, n: 'de'},
{t: eo, n: 'eo'},
{t: es, n: 'es'},
{t: fr, n: 'fr'},
{t: it, n: 'it'},
{t: nl, n: 'nl'},
{t: ru, n: 'ru'},
{t: sv, n: 'sv'},
{t: zh, n: 'zh'},
{ t: de, n: 'de' },
{ t: eo, n: 'eo' },
{ t: es, n: 'es' },
{ t: fr, n: 'fr' },
{ t: it, n: 'it' },
{ t: nl, n: 'nl' },
{ t: ru, n: 'ru' },
{ t: sv, n: 'sv' },
{ t: zh, n: 'zh' },
];
let masterKeys = Object.keys(en.translation);
@ -27,7 +27,7 @@ report += '--- | --- | ---\n';
for (let file of files) {
let keys = Object.keys(file.t.translation);
let pct: number = (keys.length / masterKeys.length * 100);
let pct: number = (keys.length / masterKeys.length) * 100;
let missing = difference(masterKeys, keys);
report += `${file.n} | ${pct.toFixed(0)}% | ${missing} \n`;
}

Loading…
Cancel
Save