DeleteAccountForm,
WebSocketJsonResponse,
GetSiteResponse,
- UserDetailsView,
UserDetailsResponse,
AddAdminResponse,
-} from '../interfaces';
+} from 'lemmy-js-client';
+import { UserDetailsView } from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
wsJsonToRes,
themes,
setTheme,
languages,
- showAvatars,
toast,
setupTippy,
+ getLanguage,
+ mdToHtml,
+ elementUrl,
+ favIconUrl,
} from '../utils';
import { UserListing } from './user-listing';
import { SortSelect } from './sort-select';
import { i18n } from '../i18next';
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;
sort: SortType;
page: number;
loading: boolean;
- avatarLoading: boolean;
userSettingsForm: UserSettingsForm;
userSettingsLoading: boolean;
deleteAccountLoading: boolean;
interface UrlParams {
view?: string;
- sort?: string;
+ sort?: SortType;
page?: number;
}
comment_score: null,
banned: null,
avatar: null,
- show_avatars: null,
- send_notifications_to_email: null,
actor_id: null,
local: null,
},
follows: [],
moderates: [],
loading: true,
- avatarLoading: false,
view: User.getViewFromProps(this.props.match.view),
sort: User.getSortTypeFromProps(this.props.match.sort),
page: User.getPageFromProps(this.props.match.page),
show_avatars: null,
send_notifications_to_email: null,
auth: null,
+ bio: null,
+ preferred_username: null,
},
userSettingsLoading: null,
deleteAccountLoading: null,
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,
},
};
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) || null;
this.state.username = this.props.match.params.username;
);
WebSocketService.Instance.getSite();
+ setupTippy();
}
get isCurrentUser() {
);
}
- static getViewFromProps(view: any): UserDetailsView {
- return view
- ? UserDetailsView[capitalizeFirstLetter(view)]
- : UserDetailsView.Overview;
+ static getViewFromProps(view: string): UserDetailsView {
+ return view ? UserDetailsView[view] : UserDetailsView.Overview;
}
- static getSortTypeFromProps(sort: any): SortType {
+ static getSortTypeFromProps(sort: string): SortType {
return sort ? routeSortTypeToEnum(sort) : SortType.New;
}
- static getPageFromProps(page: any): number {
+ static getPageFromProps(page: number): number {
return page ? Number(page) : 1;
}
// Couldnt get a refresh working. This does for now.
location.reload();
}
- setupTippy();
}
get documentTitle(): string {
if (this.state.siteRes.site.name) {
- return `/u/${this.state.username} - ${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">
- <Helmet title={this.documentTitle} />
+ <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">
- <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.username}</span>
- </h5>
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">
</svg>
</h5>
) : (
- this.selects()
+ <>
+ {this.userInfo()}
+ <hr />
+ </>
)}
+ {!this.state.loading && this.selects()}
<UserDetails
user_id={this.state.user_id}
username={this.state.username}
- sort={SortType[this.state.sort]}
+ sort={this.state.sort}
page={this.state.page}
limit={fetchLimit}
enableDownvotes={this.state.siteRes.site.enable_downvotes}
{!this.state.loading && (
<div class="col-12 col-md-4">
- {this.userInfo()}
{this.isCurrentUser && this.userSettings()}
{this.moderates()}
{this.follows()}
return (
<div class="btn-group btn-group-toggle flex-wrap mb-2">
<label
- className={`btn btn-outline-secondary pointer
+ className={`btn btn-outline-secondary pointer
${this.state.view == UserDetailsView.Overview && 'active'}
`}
>
{i18n.t('overview')}
</label>
<label
- className={`btn btn-outline-secondary pointer
+ className={`btn btn-outline-secondary pointer
${this.state.view == UserDetailsView.Comments && 'active'}
`}
>
{i18n.t('comments')}
</label>
<label
- className={`btn btn-outline-secondary pointer
+ className={`btn btn-outline-secondary pointer
${this.state.view == UserDetailsView.Posts && 'active'}
`}
>
{i18n.t('posts')}
</label>
<label
- className={`btn btn-outline-secondary pointer
+ className={`btn btn-outline-secondary pointer
${this.state.view == UserDetailsView.Saved && 'active'}
`}
>
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"
userInfo() {
let user = this.state.user;
+
return (
<div>
- <div class="card bg-transparent border-secondary mb-3">
- <div class="card-body">
- <h5>
- <ul class="list-inline mb-0">
- <li className="list-inline-item">
- <UserListing user={user} realLink />
- </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 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>
- </h5>
- <div className="d-flex align-items-center mb-2">
+ </div>
+ <div class="text-muted">
+ {i18n.t('joined')} <MomentTime data={user} showAgo />
+ </div>
+ <div className="d-flex align-items-center text-muted mb-2">
<svg class="icon">
<use xlinkHref="#icon-cake"></use>
</svg>
{moment.utc(user.published).local().format('MMM DD, YYYY')}
</span>
</div>
- <div>
- {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>
- {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"
- rel="noopener"
- 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>
<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.checkSettingsAvatar ? (
- <span class="btn 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>
- {this.checkSettingsAvatar && (
- <div class="form-group">
- <button
- class="btn btn-secondary btn-block"
- onClick={linkEvent(this, this.removeAvatar)}
- >
- {`${capitalizeFirstLetter(i18n.t('remove'))} ${i18n.t(
- 'avatar'
- )}`}
- </button>
- </div>
- )}
<div class="form-group">
<label>{i18n.t('language')}</label>
<select
<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"
- rel="noopener"
- >
+ <a href={elementUrl} target="_blank" rel="noopener">
{i18n.t('matrix_user_id')}
</a>
</label>
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
}
updateUrl(paramUpdates: UrlParams) {
const page = paramUpdates.page || this.state.page;
- const viewStr =
- paramUpdates.view || UserDetailsView[this.state.view].toLowerCase();
- const sortStr =
- paramUpdates.sort || SortType[this.state.sort].toLowerCase();
+ const viewStr = paramUpdates.view || UserDetailsView[this.state.view];
+ const sortStr = paramUpdates.sort || this.state.sort;
this.props.history.push(
`/u/${this.state.username}/view/${viewStr}/sort/${sortStr}/page/${page}`
);
}
handleSortChange(val: SortType) {
- this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
+ this.updateUrl({ sort: val, page: 1 });
}
handleViewChange(i: User, event: any) {
i.updateUrl({
- view: UserDetailsView[Number(event.target.value)].toLowerCase(),
+ view: UserDetailsView[Number(event.target.value)],
page: 1,
});
}
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 = `/pictrs/image`;
- const formData = new FormData();
- formData.append('images[]', file);
-
- i.state.avatarLoading = true;
- i.setState(i.state);
-
- fetch(imageUploadUrl, {
- method: 'POST',
- body: formData,
- })
- .then(res => res.json())
- .then(res => {
- console.log('pictrs upload:');
- console.log(res);
- if (res.msg == 'ok') {
- let hash = res.files[0].file;
- let url = `${window.location.origin}/pictrs/image/${hash}`;
- i.state.userSettingsForm.avatar = url;
- i.state.avatarLoading = false;
- i.setState(i.state);
- } else {
- i.state.avatarLoading = false;
- i.setState(i.state);
- toast(JSON.stringify(res), 'danger');
- }
- })
- .catch(error => {
- i.state.avatarLoading = false;
- i.setState(i.state);
- toast(error, 'danger');
- });
- }
-
- removeAvatar(i: User, event: any) {
- event.preventDefault();
- i.state.userSettingsLoading = true;
- i.state.userSettingsForm.avatar = '';
- i.setState(i.state);
-
- WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
- }
-
- get checkSettingsAvatar(): boolean {
- return (
- this.state.userSettingsForm.avatar &&
- this.state.userSettingsForm.avatar != ''
- );
- }
-
handleUserSettingsSubmit(i: User, event: any) {
event.preventDefault();
i.state.userSettingsLoading = true;
}
this.setState({
deleteAccountLoading: false,
- avatarLoading: false,
userSettingsLoading: false,
});
return;
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.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.matrix_user_id = this.state.user.matrix_user_id;
+ 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.SaveUserSettings) {
const data = res.data as LoginResponse;
UserService.Instance.login(data);
- this.setState({
- userSettingsLoading: false,
- });
+ 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);
+
window.scrollTo(0, 0);
} else if (res.op == UserOperation.DeleteAccount) {
this.setState({