Add user bios (#1043)

* Add user bios

* Version v0.7.35

* Add domain name change instructions to docs. (#1044)

* Add domain name change instructions to docs.

* Changing docker execs to docker-compose execs

* Set maxLength to user bio and render as md

* Fix bio updating after SaveUserSetting

Co-authored-by: Dessalines <tyhou13@gmx.com>
Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
This commit is contained in:
Azriel Lector 2020-07-31 09:08:13 +08:00 committed by GitHub
parent e31f74c3ad
commit 1acb51105a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 57 additions and 6 deletions

1
.gitignore vendored
View file

@ -16,5 +16,6 @@ ui/src/translations
# ide config # ide config
.idea/ .idea/
.vscode/
target target

View file

@ -97,6 +97,7 @@ pub struct SaveUserSettings {
lang: String, lang: String,
avatar: Option<String>, avatar: Option<String>,
email: Option<String>, email: Option<String>,
bio: Option<String>,
matrix_user_id: Option<String>, matrix_user_id: Option<String>,
new_password: Option<String>, new_password: Option<String>,
new_password_verify: Option<String>, new_password_verify: Option<String>,
@ -557,6 +558,17 @@ impl Perform for Oper<SaveUserSettings> {
None => read_user.email, None => read_user.email,
}; };
let bio = match &data.bio {
Some(bio) => {
if bio.chars().count() <= 300 {
Some(bio.to_owned())
} else {
return Err(APIError::err("bio_length_overflow").into());
}
}
None => read_user.bio,
};
let avatar = match &data.avatar { let avatar = match &data.avatar {
Some(avatar) => Some(avatar.to_owned()), Some(avatar) => Some(avatar.to_owned()),
None => read_user.avatar, None => read_user.avatar,
@ -613,7 +625,7 @@ impl Perform for Oper<SaveUserSettings> {
show_avatars: data.show_avatars, show_avatars: data.show_avatars,
send_notifications_to_email: data.send_notifications_to_email, send_notifications_to_email: data.send_notifications_to_email,
actor_id: read_user.actor_id, actor_id: read_user.actor_id,
bio: read_user.bio, bio,
local: read_user.local, local: read_user.local,
private_key: read_user.private_key, private_key: read_user.private_key,
public_key: read_user.public_key, public_key: read_user.public_key,

View file

@ -21,6 +21,7 @@ interface MarkdownTextAreaProps {
replyType?: boolean; replyType?: boolean;
focus?: boolean; focus?: boolean;
disabled?: boolean; disabled?: boolean;
maxLength?: number;
onSubmit?(msg: { val: string; formId: string }): any; onSubmit?(msg: { val: string; formId: string }): any;
onContentChange?(val: string): any; onContentChange?(val: string): any;
onReplyCancel?(): any; onReplyCancel?(): any;
@ -121,7 +122,7 @@ export class MarkdownTextArea extends Component<
required required
disabled={this.props.disabled} disabled={this.props.disabled}
rows={2} rows={2}
maxLength={10000} maxLength={this.props.maxLength || 10000}
/> />
{this.state.previewMode && ( {this.state.previewMode && (
<div <div

View file

@ -31,6 +31,7 @@ import {
toast, toast,
setupTippy, setupTippy,
getLanguage, getLanguage,
mdToHtml,
} from '../utils'; } from '../utils';
import { UserListing } from './user-listing'; import { UserListing } from './user-listing';
import { SortSelect } from './sort-select'; import { SortSelect } from './sort-select';
@ -39,6 +40,7 @@ import { MomentTime } from './moment-time';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import moment from 'moment'; import moment from 'moment';
import { UserDetails } from './user-details'; import { UserDetails } from './user-details';
import { MarkdownTextArea } from './markdown-textarea';
interface UserState { interface UserState {
user: UserView; user: UserView;
@ -109,6 +111,7 @@ export class User extends Component<any, UserState> {
show_avatars: null, show_avatars: null,
send_notifications_to_email: null, send_notifications_to_email: null,
auth: null, auth: null,
bio: null,
}, },
userSettingsLoading: null, userSettingsLoading: null,
deleteAccountLoading: null, deleteAccountLoading: null,
@ -149,7 +152,13 @@ export class User extends Component<any, UserState> {
this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind( this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
this this
); );
this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
this
);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
this.handleUserSettingsBioChange = this.handleUserSettingsBioChange.bind(
this
);
this.state.user_id = Number(this.props.match.params.id) || null; this.state.user_id = Number(this.props.match.params.id) || null;
this.state.username = this.props.match.params.username; this.state.username = this.props.match.params.username;
@ -375,6 +384,12 @@ export class User extends Component<any, UserState> {
)} )}
</ul> </ul>
</h5> </h5>
<div className="d-flex align-items-center mb-2">
<div
className="md-div"
dangerouslySetInnerHTML={mdToHtml(user.bio)}
/>
</div>
<div className="d-flex align-items-center mb-2"> <div className="d-flex align-items-center mb-2">
<svg class="icon"> <svg class="icon">
<use xlinkHref="#icon-cake"></use> <use xlinkHref="#icon-cake"></use>
@ -570,6 +585,18 @@ export class User extends Component<any, UserState> {
/> />
</div> </div>
</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}
/>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label class="col-lg-5 col-form-label"> <label class="col-lg-5 col-form-label">
<a <a
@ -900,6 +927,11 @@ export class User extends Component<any, UserState> {
i.setState(i.state); i.setState(i.state);
} }
handleUserSettingsBioChange(val: string) {
this.state.userSettingsForm.bio = val;
this.setState(this.state);
}
handleUserSettingsMatrixUserIdChange(i: User, event: any) { handleUserSettingsMatrixUserIdChange(i: User, event: any) {
i.state.userSettingsForm.matrix_user_id = event.target.value; i.state.userSettingsForm.matrix_user_id = event.target.value;
if ( if (
@ -1057,6 +1089,7 @@ export class User extends Component<any, UserState> {
this.state.userSettingsForm.lang = UserService.Instance.user.lang; this.state.userSettingsForm.lang = UserService.Instance.user.lang;
this.state.userSettingsForm.avatar = UserService.Instance.user.avatar; this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
this.state.userSettingsForm.email = this.state.user.email; this.state.userSettingsForm.email = this.state.user.email;
this.state.userSettingsForm.bio = this.state.user.bio;
this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email; this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
this.state.userSettingsForm.show_avatars = this.state.userSettingsForm.show_avatars =
UserService.Instance.user.show_avatars; UserService.Instance.user.show_avatars;
@ -1068,9 +1101,10 @@ export class User extends Component<any, UserState> {
} else if (res.op == UserOperation.SaveUserSettings) { } else if (res.op == UserOperation.SaveUserSettings) {
const data = res.data as LoginResponse; const data = res.data as LoginResponse;
UserService.Instance.login(data); UserService.Instance.login(data);
this.setState({ this.state.user.bio = this.state.userSettingsForm.bio;
userSettingsLoading: false, this.state.userSettingsLoading = false;
}); this.setState(this.state);
window.scrollTo(0, 0); window.scrollTo(0, 0);
} else if (res.op == UserOperation.DeleteAccount) { } else if (res.op == UserOperation.DeleteAccount) {
this.setState({ this.setState({

View file

@ -597,6 +597,7 @@ export interface UserSettingsForm {
lang: string; lang: string;
avatar?: string; avatar?: string;
email?: string; email?: string;
bio?: string;
matrix_user_id?: string; matrix_user_id?: string;
new_password?: string; new_password?: string;
new_password_verify?: string; new_password_verify?: string;

View file

@ -228,6 +228,7 @@
"landing_0": "landing_0":
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Thank you to our contributors: </15> dessalines, Nutomic, asonix, zacanger, and iav.", "Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Thank you to our contributors: </15> dessalines, Nutomic, asonix, zacanger, and iav.",
"not_logged_in": "Not logged in.", "not_logged_in": "Not logged in.",
"bio_length_overflow": "User bio cannot exceed 300 characters!",
"logged_in": "Logged in.", "logged_in": "Logged in.",
"must_login": "You must <1>log in or register</1> to comment.", "must_login": "You must <1>log in or register</1> to comment.",
"site_saved": "Site Saved.", "site_saved": "Site Saved.",
@ -284,5 +285,6 @@
"cake_day_info": "It's {{ creator_name }}'s cake day today!", "cake_day_info": "It's {{ creator_name }}'s cake day today!",
"invalid_post_title": "Invalid post title", "invalid_post_title": "Invalid post title",
"invalid_url": "Invalid URL.", "invalid_url": "Invalid URL.",
"play_captcha_audio": "Play Captcha Audio" "play_captcha_audio": "Play Captcha Audio",
"bio": "Bio"
} }