import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
UserOperation,
- Post,
- Comment,
CommunityUser,
- GetUserDetailsForm,
SortType,
ListingType,
- UserDetailsResponse,
UserView,
- CommentResponse,
UserSettingsForm,
LoginResponse,
- BanUserResponse,
- AddAdminResponse,
DeleteAccountForm,
- PostResponse,
WebSocketJsonResponse,
-} from '../interfaces';
+ GetSiteResponse,
+ UserDetailsResponse,
+ AddAdminResponse,
+} from 'lemmy-js-client';
+import { UserDetailsView } from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
wsJsonToRes,
themes,
setTheme,
languages,
- showAvatars,
toast,
- editCommentRes,
- saveCommentRes,
- createCommentLikeRes,
- createPostLikeFindRes,
- commentsToFlatNodes,
setupTippy,
+ getLanguage,
+ mdToHtml,
+ elementUrl,
+ favIconUrl,
} from '../utils';
-import { PostListing } from './post-listing';
import { UserListing } from './user-listing';
import { SortSelect } from './sort-select';
import { ListingTypeSelect } from './listing-type-select';
-import { CommentNodes } from './comment-nodes';
import { MomentTime } from './moment-time';
import { i18n } from '../i18next';
-
-enum View {
- Overview,
- Comments,
- Posts,
- Saved,
-}
+import moment from 'moment';
+import { UserDetails } from './user-details';
+import { MarkdownTextArea } from './markdown-textarea';
+import { ImageUploadForm } from './image-upload-form';
+import { BannerIconHeader } from './banner-icon-header';
interface UserState {
user: UserView;
username: string;
follows: Array<CommunityUser>;
moderates: Array<CommunityUser>;
- comments: Array<Comment>;
- posts: Array<Post>;
- saved?: Array<Post>;
- admins: Array<UserView>;
- view: View;
+ view: UserDetailsView;
sort: SortType;
page: number;
loading: boolean;
- avatarLoading: boolean;
userSettingsForm: UserSettingsForm;
userSettingsLoading: boolean;
deleteAccountLoading: boolean;
deleteAccountShowConfirm: boolean;
deleteAccountForm: DeleteAccountForm;
+ siteRes: GetSiteResponse;
+}
+
+interface UserProps {
+ view: UserDetailsView;
+ sort: SortType;
+ page: number;
+ user_id: number | null;
+ username: string;
+}
+
+interface UrlParams {
+ view?: string;
+ sort?: SortType;
+ page?: number;
}
export class User extends Component<any, UserState> {
comment_score: null,
banned: null,
avatar: null,
- show_avatars: null,
- send_notifications_to_email: null,
actor_id: null,
local: null,
},
username: null,
follows: [],
moderates: [],
- comments: [],
- posts: [],
- admins: [],
loading: true,
- avatarLoading: false,
- view: this.getViewFromProps(this.props),
- sort: this.getSortTypeFromProps(this.props),
- page: this.getPageFromProps(this.props),
+ view: User.getViewFromProps(this.props.match.view),
+ sort: User.getSortTypeFromProps(this.props.match.sort),
+ page: User.getPageFromProps(this.props.match.page),
userSettingsForm: {
show_nsfw: null,
theme: null,
show_avatars: null,
send_notifications_to_email: null,
auth: null,
+ bio: null,
+ preferred_username: null,
},
userSettingsLoading: null,
deleteAccountLoading: null,
deleteAccountForm: {
password: null,
},
+ siteRes: {
+ admins: [],
+ banned: [],
+ online: undefined,
+ site: {
+ id: undefined,
+ name: undefined,
+ creator_id: undefined,
+ published: undefined,
+ creator_name: undefined,
+ number_of_users: undefined,
+ number_of_posts: undefined,
+ number_of_comments: undefined,
+ number_of_communities: undefined,
+ enable_downvotes: undefined,
+ open_registration: undefined,
+ enable_nsfw: undefined,
+ icon: undefined,
+ banner: undefined,
+ creator_preferred_username: undefined,
+ },
+ version: undefined,
+ my_user: undefined,
+ federated_instances: undefined,
+ },
};
constructor(props: any, context: any) {
this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
this
);
+ this.handlePageChange = this.handlePageChange.bind(this);
+ this.handleUserSettingsBioChange = this.handleUserSettingsBioChange.bind(
+ this
+ );
+
+ this.handleAvatarUpload = this.handleAvatarUpload.bind(this);
+ this.handleAvatarRemove = this.handleAvatarRemove.bind(this);
+
+ this.handleBannerUpload = this.handleBannerUpload.bind(this);
+ this.handleBannerRemove = this.handleBannerRemove.bind(this);
- this.state.user_id = Number(this.props.match.params.id);
+ this.state.user_id = Number(this.props.match.params.id) || null;
this.state.username = this.props.match.params.username;
this.subscription = WebSocketService.Instance.subject
() => console.log('complete')
);
- this.refetch();
+ WebSocketService.Instance.getSite();
+ setupTippy();
}
get isCurrentUser() {
);
}
- getViewFromProps(props: any): View {
- return props.match.params.view
- ? View[capitalizeFirstLetter(props.match.params.view)]
- : View.Overview;
+ static getViewFromProps(view: string): UserDetailsView {
+ return view ? UserDetailsView[view] : UserDetailsView.Overview;
}
- getSortTypeFromProps(props: any): SortType {
- return props.match.params.sort
- ? routeSortTypeToEnum(props.match.params.sort)
- : SortType.New;
+ static getSortTypeFromProps(sort: string): SortType {
+ return sort ? routeSortTypeToEnum(sort) : SortType.New;
}
- getPageFromProps(props: any): number {
- return props.match.params.page ? Number(props.match.params.page) : 1;
+ static getPageFromProps(page: number): number {
+ return page ? Number(page) : 1;
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
- // Necessary for back button for some reason
- componentWillReceiveProps(nextProps: any) {
- if (
- nextProps.history.action == 'POP' ||
- nextProps.history.action == 'PUSH'
- ) {
- this.state.view = this.getViewFromProps(nextProps);
- this.state.sort = this.getSortTypeFromProps(nextProps);
- this.state.page = this.getPageFromProps(nextProps);
- this.setState(this.state);
- this.refetch();
- }
+ static getDerivedStateFromProps(props: any): UserProps {
+ return {
+ view: this.getViewFromProps(props.match.params.view),
+ sort: this.getSortTypeFromProps(props.match.params.sort),
+ page: this.getPageFromProps(props.match.params.page),
+ user_id: Number(props.match.params.id) || null,
+ username: props.match.params.username,
+ };
}
componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) {
}
}
+ get documentTitle(): string {
+ if (this.state.siteRes.site.name) {
+ return `@${this.state.username} - ${this.state.siteRes.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ get favIcon(): string {
+ return this.state.siteRes.site.icon
+ ? this.state.siteRes.site.icon
+ : favIconUrl;
+ }
+
render() {
return (
<div class="container">
- {this.state.loading ? (
- <h5>
- <svg class="icon icon-spinner spin">
- <use xlinkHref="#icon-spinner"></use>
- </svg>
- </h5>
- ) : (
- <div class="row">
- <div class="col-12 col-md-8">
+ <Helmet title={this.documentTitle}>
+ <link
+ id="favicon"
+ rel="icon"
+ type="image/x-icon"
+ href={this.favIcon}
+ />
+ </Helmet>
+ <div class="row">
+ <div class="col-12 col-md-8">
+ {this.state.loading ? (
<h5>
- {this.state.user.avatar && showAvatars() && (
- <img
- height="80"
- width="80"
- src={this.state.user.avatar}
- class="rounded-circle mr-2"
- />
- )}
- <span>/u/{this.state.user.name}</span>
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
</h5>
- {this.selects()}
- {this.state.view == View.Overview && this.overview()}
- {this.state.view == View.Comments && this.comments()}
- {this.state.view == View.Posts && this.posts()}
- {this.state.view == View.Saved && this.overview()}
- {this.paginator()}
- </div>
+ ) : (
+ <>
+ {this.userInfo()}
+ <hr />
+ </>
+ )}
+ {!this.state.loading && this.selects()}
+ <UserDetails
+ user_id={this.state.user_id}
+ username={this.state.username}
+ sort={this.state.sort}
+ page={this.state.page}
+ limit={fetchLimit}
+ enableDownvotes={this.state.siteRes.site.enable_downvotes}
+ enableNsfw={this.state.siteRes.site.enable_nsfw}
+ admins={this.state.siteRes.admins}
+ view={this.state.view}
+ onPageChange={this.handlePageChange}
+ />
+ </div>
+
+ {!this.state.loading && (
<div class="col-12 col-md-4">
- {this.userInfo()}
{this.isCurrentUser && this.userSettings()}
{this.moderates()}
{this.follows()}
</div>
- </div>
- )}
+ )}
+ </div>
</div>
);
}
viewRadios() {
return (
- <div class="btn-group btn-group-toggle">
+ <div class="btn-group btn-group-toggle flex-wrap mb-2">
<label
- className={`btn btn-sm btn-secondary pointer btn-outline-light
- ${this.state.view == View.Overview && 'active'}
+ className={`btn btn-outline-secondary pointer
+ ${this.state.view == UserDetailsView.Overview && 'active'}
`}
>
<input
type="radio"
- value={View.Overview}
- checked={this.state.view == View.Overview}
+ value={UserDetailsView.Overview}
+ checked={this.state.view === UserDetailsView.Overview}
onChange={linkEvent(this, this.handleViewChange)}
/>
{i18n.t('overview')}
</label>
<label
- className={`btn btn-sm btn-secondary pointer btn-outline-light
- ${this.state.view == View.Comments && 'active'}
+ className={`btn btn-outline-secondary pointer
+ ${this.state.view == UserDetailsView.Comments && 'active'}
`}
>
<input
type="radio"
- value={View.Comments}
- checked={this.state.view == View.Comments}
+ value={UserDetailsView.Comments}
+ checked={this.state.view == UserDetailsView.Comments}
onChange={linkEvent(this, this.handleViewChange)}
/>
{i18n.t('comments')}
</label>
<label
- className={`btn btn-sm btn-secondary pointer btn-outline-light
- ${this.state.view == View.Posts && 'active'}
+ className={`btn btn-outline-secondary pointer
+ ${this.state.view == UserDetailsView.Posts && 'active'}
`}
>
<input
type="radio"
- value={View.Posts}
- checked={this.state.view == View.Posts}
+ value={UserDetailsView.Posts}
+ checked={this.state.view == UserDetailsView.Posts}
onChange={linkEvent(this, this.handleViewChange)}
/>
{i18n.t('posts')}
</label>
<label
- className={`btn btn-sm btn-secondary pointer btn-outline-light
- ${this.state.view == View.Saved && 'active'}
+ className={`btn btn-outline-secondary pointer
+ ${this.state.view == UserDetailsView.Saved && 'active'}
`}
>
<input
type="radio"
- value={View.Saved}
- checked={this.state.view == View.Saved}
+ value={UserDetailsView.Saved}
+ checked={this.state.view == UserDetailsView.Saved}
onChange={linkEvent(this, this.handleViewChange)}
/>
{i18n.t('saved')}
hideHot
/>
<a
- href={`/feeds/u/${this.state.username}.xml?sort=${
- SortType[this.state.sort]
- }`}
+ href={`/feeds/u/${this.state.username}.xml?sort=${this.state.sort}`}
target="_blank"
+ rel="noopener"
title="RSS"
>
<svg class="icon mx-2 text-muted small">
);
}
- overview() {
- let combined: Array<{ type_: string; data: Comment | Post }> = [];
- let comments = this.state.comments.map(e => {
- return { type_: 'comments', data: e };
- });
- let posts = this.state.posts.map(e => {
- return { type_: 'posts', data: e };
- });
-
- combined.push(...comments);
- combined.push(...posts);
-
- // Sort it
- if (this.state.sort == SortType.New) {
- combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
- } else {
- combined.sort((a, b) => b.data.score - a.data.score);
- }
-
- return (
- <div>
- {combined.map(i => (
- <div>
- {i.type_ == 'posts' ? (
- <PostListing
- post={i.data as Post}
- admins={this.state.admins}
- showCommunity
- />
- ) : (
- <CommentNodes
- nodes={[{ comment: i.data as Comment }]}
- admins={this.state.admins}
- noIndent
- showContext
- />
- )}
- </div>
- ))}
- </div>
- );
- }
-
- comments() {
- return (
- <div>
- <CommentNodes
- nodes={commentsToFlatNodes(this.state.comments)}
- admins={this.state.admins}
- noIndent
- showContext
- />
- </div>
- );
- }
-
- posts() {
- return (
- <div>
- {this.state.posts.map(post => (
- <PostListing post={post} admins={this.state.admins} showCommunity />
- ))}
- </div>
- );
- }
-
userInfo() {
let user = this.state.user;
+
return (
<div>
- <div class="card border-secondary mb-3">
- <div class="card-body">
- <h5>
- <ul class="list-inline mb-0">
- <li className="list-inline-item">
- <UserListing user={user} />
- </li>
- {user.banned && (
- <li className="list-inline-item badge badge-danger">
- {i18n.t('banned')}
- </li>
+ <BannerIconHeader
+ banner={this.state.user.banner}
+ icon={this.state.user.avatar}
+ />
+ <div class="mb-3">
+ <div class="">
+ <div class="mb-0 d-flex flex-wrap">
+ <div>
+ {user.preferred_username && (
+ <h5 class="mb-0">{user.preferred_username}</h5>
)}
- </ul>
- </h5>
+ <ul class="list-inline mb-2">
+ <li className="list-inline-item">
+ <UserListing
+ user={user}
+ realLink
+ useApubName
+ muted
+ hideAvatar
+ />
+ </li>
+ {user.banned && (
+ <li className="list-inline-item badge badge-danger">
+ {i18n.t('banned')}
+ </li>
+ )}
+ </ul>
+ </div>
+ <div className="flex-grow-1 unselectable pointer mx-2"></div>
+ {this.isCurrentUser ? (
+ <button
+ class="d-flex align-self-start btn btn-secondary ml-2"
+ onClick={linkEvent(this, this.handleLogoutClick)}
+ >
+ {i18n.t('logout')}
+ </button>
+ ) : (
+ <>
+ <a
+ className={`d-flex align-self-start btn btn-secondary ml-2 ${
+ !this.state.user.matrix_user_id && 'invisible'
+ }`}
+ target="_blank"
+ rel="noopener"
+ href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
+ >
+ {i18n.t('send_secure_message')}
+ </a>
+ <Link
+ class="d-flex align-self-start btn btn-secondary ml-2"
+ to={`/create_private_message?recipient_id=${this.state.user.id}`}
+ >
+ {i18n.t('send_message')}
+ </Link>
+ </>
+ )}
+ </div>
+ {user.bio && (
+ <div className="d-flex align-items-center mb-2">
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(user.bio)}
+ />
+ </div>
+ )}
<div>
+ <ul class="list-inline mb-2">
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_posts', { count: user.number_of_posts })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_comments', {
+ count: user.number_of_comments,
+ })}
+ </li>
+ </ul>
+ </div>
+ <div class="text-muted">
{i18n.t('joined')} <MomentTime data={user} showAgo />
</div>
- <div class="table-responsive mt-1">
- <table class="table table-bordered table-sm mt-2 mb-0">
- {/*
- <tr>
- <td class="text-center" colSpan={2}>
- {i18n.t('number_of_points', {
- count: user.post_score + user.comment_score,
- })}
- </td>
- </tr>
- */}
- <tr>
- {/*
- <td>
- {i18n.t('number_of_points', { count: user.post_score })}
- </td>
- */}
- <td>
- {i18n.t('number_of_posts', { count: user.number_of_posts })}
- </td>
- {/*
- </tr>
- <tr>
- <td>
- {i18n.t('number_of_points', { count: user.comment_score })}
- </td>
- */}
- <td>
- {i18n.t('number_of_comments', {
- count: user.number_of_comments,
- })}
- </td>
- </tr>
- </table>
+ <div className="d-flex align-items-center text-muted mb-2">
+ <svg class="icon">
+ <use xlinkHref="#icon-cake"></use>
+ </svg>
+ <span className="ml-2">
+ {i18n.t('cake_day_title')}{' '}
+ {moment.utc(user.published).local().format('MMM DD, YYYY')}
+ </span>
</div>
- {this.isCurrentUser ? (
- <button
- class="btn btn-block btn-secondary mt-3"
- onClick={linkEvent(this, this.handleLogoutClick)}
- >
- {i18n.t('logout')}
- </button>
- ) : (
- <>
- <a
- className={`btn btn-block btn-secondary mt-3 ${
- !this.state.user.matrix_user_id && 'disabled'
- }`}
- target="_blank"
- href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
- >
- {i18n.t('send_secure_message')}
- </a>
- <Link
- class="btn btn-block btn-secondary mt-3"
- to={`/create_private_message?recipient_id=${this.state.user.id}`}
- >
- {i18n.t('send_message')}
- </Link>
- </>
- )}
</div>
</div>
</div>
userSettings() {
return (
<div>
- <div class="card border-secondary mb-3">
+ <div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5>{i18n.t('settings')}</h5>
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
<div class="form-group">
<label>{i18n.t('avatar')}</label>
- <form class="d-inline">
- <label
- htmlFor="file-upload"
- class="pointer ml-4 text-muted small font-weight-bold"
- >
- {!this.state.userSettingsForm.avatar ? (
- <span class="btn btn-sm btn-secondary">
- {i18n.t('upload_avatar')}
- </span>
- ) : (
- <img
- height="80"
- width="80"
- src={this.state.userSettingsForm.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>
+ <ImageUploadForm
+ uploadTitle={i18n.t('upload_avatar')}
+ imageSrc={this.state.userSettingsForm.avatar}
+ onUpload={this.handleAvatarUpload}
+ onRemove={this.handleAvatarRemove}
+ rounded
+ />
+ </div>
+ <div class="form-group">
+ <label>{i18n.t('banner')}</label>
+ <ImageUploadForm
+ uploadTitle={i18n.t('upload_banner')}
+ imageSrc={this.state.userSettingsForm.banner}
+ onUpload={this.handleBannerUpload}
+ onRemove={this.handleBannerRemove}
+ />
</div>
<div class="form-group">
<label>{i18n.t('language')}</label>
<select
value={this.state.userSettingsForm.lang}
onChange={linkEvent(this, this.handleUserSettingsLangChange)}
- class="ml-2 custom-select custom-select-sm w-auto"
+ class="ml-2 custom-select w-auto"
>
<option disabled>{i18n.t('language')}</option>
<option value="browser">{i18n.t('browser_default')}</option>
<select
value={this.state.userSettingsForm.theme}
onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
- class="ml-2 custom-select custom-select-sm w-auto"
+ class="ml-2 custom-select w-auto"
>
<option disabled>{i18n.t('theme')}</option>
{themes.map(theme => (
<div class="mr-2">{i18n.t('sort_type')}</div>
</label>
<ListingTypeSelect
- type_={this.state.userSettingsForm.default_listing_type}
+ type_={
+ Object.values(ListingType)[
+ this.state.userSettingsForm.default_listing_type
+ ]
+ }
+ showLocal={
+ this.state.siteRes.federated_instances &&
+ this.state.siteRes.federated_instances.length > 0
+ }
onChange={this.handleUserSettingsListingTypeChange}
/>
</form>
<div class="mr-2">{i18n.t('type')}</div>
</label>
<SortSelect
- sort={this.state.userSettingsForm.default_sort_type}
+ sort={
+ Object.values(SortType)[
+ this.state.userSettingsForm.default_sort_type
+ ]
+ }
onChange={this.handleUserSettingsSortTypeChange}
/>
</form>
+ <div class="form-group row">
+ <label class="col-lg-5 col-form-label">
+ {i18n.t('display_name')}
+ </label>
+ <div class="col-lg-7">
+ <input
+ type="text"
+ class="form-control"
+ placeholder={i18n.t('optional')}
+ value={this.state.userSettingsForm.preferred_username}
+ onInput={linkEvent(
+ this,
+ this.handleUserSettingsPreferredUsernameChange
+ )}
+ pattern="^(?!@)(.+)$"
+ minLength={3}
+ maxLength={20}
+ />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-lg-3 col-form-label" htmlFor="user-bio">
+ {i18n.t('bio')}
+ </label>
+ <div class="col-lg-9">
+ <MarkdownTextArea
+ initialContent={this.state.userSettingsForm.bio}
+ onContentChange={this.handleUserSettingsBioChange}
+ maxLength={300}
+ hideNavigationWarnings
+ />
+ </div>
+ </div>
<div class="form-group row">
<label class="col-lg-3 col-form-label" htmlFor="user-email">
{i18n.t('email')}
</div>
<div class="form-group row">
<label class="col-lg-5 col-form-label">
- <a href="https://about.riot.im/" target="_blank">
+ <a href={elementUrl} target="_blank" rel="noopener">
{i18n.t('matrix_user_id')}
</a>
</label>
id="user-password"
class="form-control"
value={this.state.userSettingsForm.new_password}
+ autoComplete="new-password"
onInput={linkEvent(
this,
this.handleUserSettingsNewPasswordChange
id="user-verify-password"
class="form-control"
value={this.state.userSettingsForm.new_password_verify}
+ autoComplete="new-password"
onInput={linkEvent(
this,
this.handleUserSettingsNewPasswordVerifyChange
id="user-old-password"
class="form-control"
value={this.state.userSettingsForm.old_password}
+ autoComplete="new-password"
onInput={linkEvent(
this,
this.handleUserSettingsOldPasswordChange
/>
</div>
</div>
- {WebSocketService.Instance.site.enable_nsfw && (
+ {this.state.siteRes.site.enable_nsfw && (
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
id="user-send-notifications-to-email"
type="checkbox"
- disabled={!this.state.user.email}
+ disabled={!this.state.userSettingsForm.email}
checked={
this.state.userSettingsForm.send_notifications_to_email
}
<input
type="password"
value={this.state.deleteAccountForm.password}
+ autoComplete="new-password"
onInput={linkEvent(
this,
this.handleDeleteAccountPasswordChange
return (
<div>
{this.state.moderates.length > 0 && (
- <div class="card border-secondary mb-3">
+ <div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5>{i18n.t('moderates')}</h5>
<ul class="list-unstyled mb-0">
return (
<div>
{this.state.follows.length > 0 && (
- <div class="card border-secondary mb-3">
+ <div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5>{i18n.t('subscribed')}</h5>
<ul class="list-unstyled mb-0">
);
}
- paginator() {
- return (
- <div class="my-2">
- {this.state.page > 1 && (
- <button
- class="btn btn-sm btn-secondary mr-1"
- onClick={linkEvent(this, this.prevPage)}
- >
- {i18n.t('prev')}
- </button>
- )}
- <button
- class="btn btn-sm btn-secondary"
- onClick={linkEvent(this, this.nextPage)}
- >
- {i18n.t('next')}
- </button>
- </div>
- );
- }
-
- updateUrl() {
- let viewStr = View[this.state.view].toLowerCase();
- let sortStr = SortType[this.state.sort].toLowerCase();
+ updateUrl(paramUpdates: UrlParams) {
+ const page = paramUpdates.page || this.state.page;
+ const viewStr = paramUpdates.view || UserDetailsView[this.state.view];
+ const sortStr = paramUpdates.sort || this.state.sort;
this.props.history.push(
- `/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
+ `/u/${this.state.username}/view/${viewStr}/sort/${sortStr}/page/${page}`
);
}
- nextPage(i: User) {
- i.state.page++;
- i.setState(i.state);
- i.updateUrl();
- i.refetch();
- }
-
- prevPage(i: User) {
- i.state.page--;
- i.setState(i.state);
- i.updateUrl();
- i.refetch();
- }
-
- refetch() {
- let form: GetUserDetailsForm = {
- user_id: this.state.user_id,
- username: this.state.username,
- sort: SortType[this.state.sort],
- saved_only: this.state.view == View.Saved,
- page: this.state.page,
- limit: fetchLimit,
- };
- WebSocketService.Instance.getUserDetails(form);
+ handlePageChange(page: number) {
+ this.updateUrl({ page });
}
handleSortChange(val: SortType) {
- this.state.sort = val;
- this.state.page = 1;
- this.setState(this.state);
- this.updateUrl();
- this.refetch();
+ this.updateUrl({ sort: val, page: 1 });
}
handleViewChange(i: User, event: any) {
- i.state.view = Number(event.target.value);
- i.state.page = 1;
- i.setState(i.state);
- i.updateUrl();
- i.refetch();
+ i.updateUrl({
+ view: UserDetailsView[Number(event.target.value)],
+ page: 1,
+ });
}
handleUserSettingsShowNsfwChange(i: User, event: any) {
handleUserSettingsThemeChange(i: User, event: any) {
i.state.userSettingsForm.theme = event.target.value;
- setTheme(event.target.value);
+ setTheme(event.target.value, true);
i.setState(i.state);
}
handleUserSettingsLangChange(i: User, event: any) {
i.state.userSettingsForm.lang = event.target.value;
- i18n.changeLanguage(i.state.userSettingsForm.lang);
+ i18n.changeLanguage(getLanguage(i.state.userSettingsForm.lang));
i.setState(i.state);
}
handleUserSettingsSortTypeChange(val: SortType) {
- this.state.userSettingsForm.default_sort_type = val;
+ this.state.userSettingsForm.default_sort_type = Object.keys(
+ SortType
+ ).indexOf(val);
this.setState(this.state);
}
handleUserSettingsListingTypeChange(val: ListingType) {
- this.state.userSettingsForm.default_listing_type = val;
+ this.state.userSettingsForm.default_listing_type = Object.keys(
+ ListingType
+ ).indexOf(val);
this.setState(this.state);
}
handleUserSettingsEmailChange(i: User, event: any) {
i.state.userSettingsForm.email = event.target.value;
- if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
- i.state.userSettingsForm.email = undefined;
- }
+ i.setState(i.state);
+ }
+
+ handleUserSettingsBioChange(val: string) {
+ this.state.userSettingsForm.bio = val;
+ this.setState(this.state);
+ }
+
+ handleAvatarUpload(url: string) {
+ this.state.userSettingsForm.avatar = url;
+ this.setState(this.state);
+ }
+
+ handleAvatarRemove() {
+ this.state.userSettingsForm.avatar = '';
+ this.setState(this.state);
+ }
+
+ handleBannerUpload(url: string) {
+ this.state.userSettingsForm.banner = url;
+ this.setState(this.state);
+ }
+
+ handleBannerRemove() {
+ this.state.userSettingsForm.banner = '';
+ this.setState(this.state);
+ }
+
+ handleUserSettingsPreferredUsernameChange(i: User, event: any) {
+ i.state.userSettingsForm.preferred_username = event.target.value;
i.setState(i.state);
}
i.setState(i.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);
- toast(error, 'danger');
- });
- }
-
handleUserSettingsSubmit(i: User, event: any) {
event.preventDefault();
i.state.userSettingsLoading = true;
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
- let res = wsJsonToRes(msg);
+ const res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
- this.state.deleteAccountLoading = false;
- this.state.avatarLoading = false;
- this.state.userSettingsLoading = false;
if (msg.error == 'couldnt_find_that_username_or_email') {
this.context.router.history.push('/');
}
- this.setState(this.state);
+ this.setState({
+ deleteAccountLoading: false,
+ userSettingsLoading: false,
+ });
return;
- } else if (msg.reconnect) {
- this.refetch();
} else if (res.op == UserOperation.GetUserDetails) {
- let data = res.data as UserDetailsResponse;
- this.state.user = data.user;
- this.state.comments = data.comments;
- this.state.follows = data.follows;
- this.state.moderates = data.moderates;
- this.state.posts = data.posts;
- this.state.admins = data.admins;
- this.state.loading = false;
- if (this.isCurrentUser) {
- this.state.userSettingsForm.show_nsfw =
- UserService.Instance.user.show_nsfw;
- this.state.userSettingsForm.theme = UserService.Instance.user.theme
- ? UserService.Instance.user.theme
- : 'darkly';
- this.state.userSettingsForm.default_sort_type =
- UserService.Instance.user.default_sort_type;
- 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;
- this.state.userSettingsForm.email = this.state.user.email;
- this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
- this.state.userSettingsForm.show_avatars =
- UserService.Instance.user.show_avatars;
- this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
- }
- document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
- window.scrollTo(0, 0);
- this.setState(this.state);
- setupTippy();
- } else if (res.op == UserOperation.EditComment) {
- let data = res.data as CommentResponse;
- editCommentRes(data, this.state.comments);
- this.setState(this.state);
- } else if (res.op == UserOperation.CreateComment) {
- let data = res.data as CommentResponse;
- if (
- UserService.Instance.user &&
- data.comment.creator_id == UserService.Instance.user.id
- ) {
- toast(i18n.t('reply_sent'));
+ // Since the UserDetails contains posts/comments as well as some general user info we listen here as well
+ // and set the parent state if it is not set or differs
+ const data = res.data as UserDetailsResponse;
+
+ if (this.state.user.id !== data.user.id) {
+ this.state.user = data.user;
+ this.state.follows = data.follows;
+ this.state.moderates = data.moderates;
+
+ if (this.isCurrentUser) {
+ this.state.userSettingsForm.show_nsfw =
+ UserService.Instance.user.show_nsfw;
+ this.state.userSettingsForm.theme = UserService.Instance.user.theme
+ ? UserService.Instance.user.theme
+ : 'darkly';
+ this.state.userSettingsForm.default_sort_type =
+ UserService.Instance.user.default_sort_type;
+ 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;
+ this.state.userSettingsForm.banner = UserService.Instance.user.banner;
+ this.state.userSettingsForm.preferred_username =
+ UserService.Instance.user.preferred_username;
+ this.state.userSettingsForm.show_avatars =
+ UserService.Instance.user.show_avatars;
+ this.state.userSettingsForm.email = UserService.Instance.user.email;
+ this.state.userSettingsForm.bio = UserService.Instance.user.bio;
+ this.state.userSettingsForm.send_notifications_to_email =
+ UserService.Instance.user.send_notifications_to_email;
+ this.state.userSettingsForm.matrix_user_id =
+ UserService.Instance.user.matrix_user_id;
+ }
+ this.state.loading = false;
+ this.setState(this.state);
}
- } else if (res.op == UserOperation.SaveComment) {
- let data = res.data as CommentResponse;
- saveCommentRes(data, this.state.comments);
- this.setState(this.state);
- } else if (res.op == UserOperation.CreateCommentLike) {
- let data = res.data as CommentResponse;
- createCommentLikeRes(data, this.state.comments);
- this.setState(this.state);
- } else if (res.op == UserOperation.CreatePostLike) {
- let data = res.data as PostResponse;
- createPostLikeFindRes(data, this.state.posts);
- this.setState(this.state);
- } else if (res.op == UserOperation.BanUser) {
- let data = res.data as BanUserResponse;
- this.state.comments
- .filter(c => c.creator_id == data.user.id)
- .forEach(c => (c.banned = data.banned));
- this.state.posts
- .filter(c => c.creator_id == data.user.id)
- .forEach(c => (c.banned = data.banned));
- this.setState(this.state);
- } else if (res.op == UserOperation.AddAdmin) {
- let data = res.data as AddAdminResponse;
- this.state.admins = data.admins;
- this.setState(this.state);
} else if (res.op == UserOperation.SaveUserSettings) {
- let data = res.data as LoginResponse;
- this.state = this.emptyState;
+ const data = res.data as LoginResponse;
+ UserService.Instance.login(data);
+ this.state.user.bio = this.state.userSettingsForm.bio;
+ this.state.user.preferred_username = this.state.userSettingsForm.preferred_username;
+ this.state.user.banner = this.state.userSettingsForm.banner;
+ this.state.user.avatar = this.state.userSettingsForm.avatar;
this.state.userSettingsLoading = false;
this.setState(this.state);
- UserService.Instance.login(data);
+
+ window.scrollTo(0, 0);
} else if (res.op == UserOperation.DeleteAccount) {
- this.state.deleteAccountLoading = false;
- this.state.deleteAccountShowConfirm = false;
- this.setState(this.state);
+ this.setState({
+ deleteAccountLoading: false,
+ deleteAccountShowConfirm: false,
+ });
this.context.router.history.push('/');
+ } else if (res.op == UserOperation.GetSite) {
+ const data = res.data as GetSiteResponse;
+ this.state.siteRes = data;
+ this.setState(this.state);
+ } else if (res.op == UserOperation.AddAdmin) {
+ const data = res.data as AddAdminResponse;
+ this.state.siteRes.admins = data.admins;
+ this.setState(this.state);
}
}
}