lang | done | missing
--- | --- | ---
-de | 97% | avatar,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
-eo | 84% | number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,are_you_sure,yes,no
-es | 92% | avatar,archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
-fr | 92% | avatar,archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
-it | 93% | avatar,archive_link,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
-nl | 86% | preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme
-ru | 80% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
-sv | 92% | avatar,archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
-zh | 78% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
-
+de | 97% | avatar,old_password,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
+eo | 83% | number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,are_you_sure,yes,no
+es | 92% | avatar,archive_link,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
+fr | 92% | avatar,archive_link,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
+it | 93% | avatar,archive_link,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
+nl | 85% | preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme
+ru | 79% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
+sv | 92% | avatar,archive_link,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
+zh | 77% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
If you'd like to update this report, run:
--- /dev/null
+-- user
+drop view user_view;
+create view user_view as
+select id,
+name,
+avatar,
+fedi_name,
+admin,
+banned,
+published,
+(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
+(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
+(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
+(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
+from user_ u;
--- /dev/null
+-- user
+drop view user_view;
+create view user_view as
+select id,
+name,
+avatar,
+email,
+fedi_name,
+admin,
+banned,
+published,
+(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
+(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
+(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
+(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
+from user_ u;
default_listing_type: i16,
lang: String,
avatar: Option<String>,
+ email: Option<String>,
+ new_password: Option<String>,
+ new_password_verify: Option<String>,
+ old_password: Option<String>,
auth: String,
}
let read_user = User_::read(&conn, user_id)?;
+ let email = match &data.email {
+ Some(email) => Some(email.to_owned()),
+ None => read_user.email,
+ };
+
+ let password_encrypted = match &data.new_password {
+ Some(new_password) => {
+ match &data.new_password_verify {
+ Some(new_password_verify) => {
+ // Make sure passwords match
+ if new_password != new_password_verify {
+ return Err(APIError::err(&self.op, "passwords_dont_match"))?;
+ }
+
+ // Check the old password
+ match &data.old_password {
+ Some(old_password) => {
+ let valid: bool =
+ verify(old_password, &read_user.password_encrypted).unwrap_or(false);
+ if !valid {
+ return Err(APIError::err(&self.op, "password_incorrect"))?;
+ }
+ User_::update_password(&conn, user_id, &new_password)?.password_encrypted
+ }
+ None => return Err(APIError::err(&self.op, "password_incorrect"))?,
+ }
+ }
+ None => return Err(APIError::err(&self.op, "passwords_dont_match"))?,
+ }
+ }
+ None => read_user.password_encrypted,
+ };
+
let user_form = UserForm {
name: read_user.name,
fedi_name: read_user.fedi_name,
- email: read_user.email,
+ email,
avatar: data.avatar.to_owned(),
- password_encrypted: read_user.password_encrypted,
+ password_encrypted,
preferred_username: read_user.preferred_username,
updated: Some(naive_now()),
admin: read_user.admin,
return Err(APIError::err(&self.op, "passwords_dont_match"))?;
}
- // Fetch the user
- let read_user = User_::read(&conn, user_id)?;
-
// Update the user with the new password
- let user_form = UserForm {
- name: read_user.name,
- fedi_name: read_user.fedi_name,
- email: read_user.email,
- avatar: read_user.avatar,
- password_encrypted: data.password.to_owned(),
- preferred_username: read_user.preferred_username,
- updated: Some(naive_now()),
- admin: read_user.admin,
- banned: read_user.banned,
- show_nsfw: read_user.show_nsfw,
- theme: read_user.theme,
- default_sort_type: read_user.default_sort_type,
- default_listing_type: read_user.default_listing_type,
- lang: read_user.lang,
- };
-
- let updated_user = match User_::update_password(&conn, user_id, &user_form) {
+ let updated_user = match User_::update_password(&conn, user_id, &data.password) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
};
pub fn update_password(
conn: &PgConnection,
user_id: i32,
- form: &UserForm,
+ new_password: &str,
) -> Result<Self, Error> {
- let mut edited_user = form.clone();
- let password_hash =
- hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
- edited_user.password_encrypted = password_hash;
+ let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
- Self::update(&conn, user_id, &edited_user)
+ diesel::update(user_.find(user_id))
+ .set(password_encrypted.eq(password_hash))
+ .get_result::<Self>(conn)
}
pub fn read_from_name(conn: &PgConnection, from_user_name: String) -> Result<Self, Error> {
id -> Int4,
name -> Varchar,
avatar -> Nullable<Text>,
+ email -> Nullable<Text>,
fedi_name -> Varchar,
admin -> Bool,
banned -> Bool,
pub id: i32,
pub name: String,
pub avatar: Option<String>,
+ pub email: Option<String>,
pub fedi_name: String,
pub admin: bool,
pub banned: bool,
default_sort_type: null,
default_listing_type: null,
lang: null,
- avatar: null,
auth: null,
},
userSettingsLoading: null,
</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)}
+ <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"
/>
- </form>
- </div>
+ </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 class="form-group">
- <div class="col-12">
- <label>
+ <label>
+ <T i18nKey="language">#</T>
+ </label>
+ <select
+ value={this.state.userSettingsForm.lang}
+ onChange={linkEvent(this, this.handleUserSettingsLangChange)}
+ class="ml-2 custom-select custom-select-sm w-auto"
+ >
+ <option disabled>
<T i18nKey="language">#</T>
- </label>
- <select
- value={this.state.userSettingsForm.lang}
- onChange={linkEvent(
+ </option>
+ <option value="browser">
+ <T i18nKey="browser_default">#</T>
+ </option>
+ <option disabled>──</option>
+ {languages.map(lang => (
+ <option value={lang.code}>{lang.name}</option>
+ ))}
+ </select>
+ </div>
+ <div class="form-group">
+ <label>
+ <T i18nKey="theme">#</T>
+ </label>
+ <select
+ value={this.state.userSettingsForm.theme}
+ onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
+ class="ml-2 custom-select custom-select-sm w-auto"
+ >
+ <option disabled>
+ <T i18nKey="theme">#</T>
+ </option>
+ {themes.map(theme => (
+ <option value={theme}>{theme}</option>
+ ))}
+ </select>
+ </div>
+ <form className="form-group">
+ <label>
+ <T i18nKey="sort_type" class="mr-2">
+ #
+ </T>
+ </label>
+ <ListingTypeSelect
+ type_={this.state.userSettingsForm.default_listing_type}
+ onChange={this.handleUserSettingsListingTypeChange}
+ />
+ </form>
+ <form className="form-group">
+ <label>
+ <T i18nKey="type" class="mr-2">
+ #
+ </T>
+ </label>
+ <SortSelect
+ sort={this.state.userSettingsForm.default_sort_type}
+ onChange={this.handleUserSettingsSortTypeChange}
+ />
+ </form>
+ <div class="form-group row">
+ <label class="col-lg-3 col-form-label">
+ <T i18nKey="email">#</T>
+ </label>
+ <div class="col-lg-9">
+ <input
+ type="email"
+ class="form-control"
+ placeholder={i18n.t('optional')}
+ value={this.state.userSettingsForm.email}
+ onInput={linkEvent(
this,
- this.handleUserSettingsLangChange
+ this.handleUserSettingsEmailChange
)}
- class="ml-2 custom-select custom-select-sm w-auto"
- >
- <option disabled>
- <T i18nKey="language">#</T>
- </option>
- <option value="browser">
- <T i18nKey="browser_default">#</T>
- </option>
- <option disabled>──</option>
- {languages.map(lang => (
- <option value={lang.code}>{lang.name}</option>
- ))}
- </select>
+ minLength={3}
+ />
</div>
</div>
- <div class="form-group">
- <div class="col-12">
- <label>
- <T i18nKey="theme">#</T>
- </label>
- <select
- value={this.state.userSettingsForm.theme}
- onChange={linkEvent(
+ <div class="form-group row">
+ <label class="col-lg-5 col-form-label">
+ <T i18nKey="new_password">#</T>
+ </label>
+ <div class="col-lg-7">
+ <input
+ type="password"
+ class="form-control"
+ value={this.state.userSettingsForm.new_password}
+ onInput={linkEvent(
this,
- this.handleUserSettingsThemeChange
+ this.handleUserSettingsNewPasswordChange
)}
- class="ml-2 custom-select custom-select-sm w-auto"
- >
- <option disabled>
- <T i18nKey="theme">#</T>
- </option>
- {themes.map(theme => (
- <option value={theme}>{theme}</option>
- ))}
- </select>
+ />
</div>
</div>
- <form className="form-group">
- <div class="col-12">
- <label>
- <T i18nKey="sort_type" class="mr-2">
- #
- </T>
- </label>
- <ListingTypeSelect
- type_={this.state.userSettingsForm.default_listing_type}
- onChange={this.handleUserSettingsListingTypeChange}
+ <div class="form-group row">
+ <label class="col-lg-5 col-form-label">
+ <T i18nKey="verify_password">#</T>
+ </label>
+ <div class="col-lg-7">
+ <input
+ type="password"
+ class="form-control"
+ value={this.state.userSettingsForm.new_password_verify}
+ onInput={linkEvent(
+ this,
+ this.handleUserSettingsNewPasswordVerifyChange
+ )}
/>
</div>
- </form>
- <form className="form-group">
- <div class="col-12">
- <label>
- <T i18nKey="type" class="mr-2">
- #
- </T>
- </label>
- <SortSelect
- sort={this.state.userSettingsForm.default_sort_type}
- onChange={this.handleUserSettingsSortTypeChange}
+ </div>
+ <div class="form-group row">
+ <label class="col-lg-5 col-form-label">
+ <T i18nKey="old_password">#</T>
+ </label>
+ <div class="col-lg-7">
+ <input
+ type="password"
+ class="form-control"
+ value={this.state.userSettingsForm.old_password}
+ onInput={linkEvent(
+ this,
+ this.handleUserSettingsOldPasswordChange
+ )}
/>
</div>
- </form>
+ </div>
{WebSocketService.Instance.site.enable_nsfw && (
<div class="form-group">
- <div class="col-12">
- <div class="form-check">
- <input
- class="form-check-input"
- type="checkbox"
- checked={this.state.userSettingsForm.show_nsfw}
- onChange={linkEvent(
- this,
- this.handleUserSettingsShowNsfwChange
- )}
- />
- <label class="form-check-label">
- <T i18nKey="show_nsfw">#</T>
- </label>
- </div>
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ type="checkbox"
+ checked={this.state.userSettingsForm.show_nsfw}
+ onChange={linkEvent(
+ this,
+ this.handleUserSettingsShowNsfwChange
+ )}
+ />
+ <label class="form-check-label">
+ <T i18nKey="show_nsfw">#</T>
+ </label>
</div>
</div>
)}
<div class="form-group">
- <div class="col-12">
- <button
- type="submit"
- class="btn btn-block btn-secondary mr-4"
- >
- {this.state.userSettingsLoading ? (
- <svg class="icon icon-spinner spin">
- <use xlinkHref="#icon-spinner"></use>
- </svg>
- ) : (
- capitalizeFirstLetter(i18n.t('save'))
- )}
- </button>
- </div>
+ <button type="submit" class="btn btn-block btn-secondary mr-4">
+ {this.state.userSettingsLoading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ capitalizeFirstLetter(i18n.t('save'))
+ )}
+ </button>
</div>
<hr />
<div class="form-group mb-0">
- <div class="col-12">
- <button
- class="btn btn-block btn-danger"
- onClick={linkEvent(
- this,
- this.handleDeleteAccountShowConfirmToggle
- )}
- >
- <T i18nKey="delete_account">#</T>
- </button>
- {this.state.deleteAccountShowConfirm && (
- <>
- <div class="my-2 alert alert-danger" role="alert">
- <T i18nKey="delete_account_confirm">#</T>
- </div>
- <input
- type="password"
- value={this.state.deleteAccountForm.password}
- onInput={linkEvent(
- this,
- this.handleDeleteAccountPasswordChange
- )}
- class="form-control my-2"
- />
- <button
- class="btn btn-danger mr-4"
- disabled={!this.state.deleteAccountForm.password}
- onClick={linkEvent(this, this.handleDeleteAccount)}
- >
- {this.state.deleteAccountLoading ? (
- <svg class="icon icon-spinner spin">
- <use xlinkHref="#icon-spinner"></use>
- </svg>
- ) : (
- capitalizeFirstLetter(i18n.t('delete'))
- )}
- </button>
- <button
- class="btn btn-secondary"
- onClick={linkEvent(
- this,
- this.handleDeleteAccountShowConfirmToggle
- )}
- >
- <T i18nKey="cancel">#</T>
- </button>
- </>
+ <button
+ class="btn btn-block btn-danger"
+ onClick={linkEvent(
+ this,
+ this.handleDeleteAccountShowConfirmToggle
)}
- </div>
+ >
+ <T i18nKey="delete_account">#</T>
+ </button>
+ {this.state.deleteAccountShowConfirm && (
+ <>
+ <div class="my-2 alert alert-danger" role="alert">
+ <T i18nKey="delete_account_confirm">#</T>
+ </div>
+ <input
+ type="password"
+ value={this.state.deleteAccountForm.password}
+ onInput={linkEvent(
+ this,
+ this.handleDeleteAccountPasswordChange
+ )}
+ class="form-control my-2"
+ />
+ <button
+ class="btn btn-danger mr-4"
+ disabled={!this.state.deleteAccountForm.password}
+ onClick={linkEvent(this, this.handleDeleteAccount)}
+ >
+ {this.state.deleteAccountLoading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ capitalizeFirstLetter(i18n.t('delete'))
+ )}
+ </button>
+ <button
+ class="btn btn-secondary"
+ onClick={linkEvent(
+ this,
+ this.handleDeleteAccountShowConfirmToggle
+ )}
+ >
+ <T i18nKey="cancel">#</T>
+ </button>
+ </>
+ )}
</div>
</form>
</div>
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);
+ }
+
+ handleUserSettingsNewPasswordChange(i: User, event: any) {
+ i.state.userSettingsForm.new_password = event.target.value;
+ if (i.state.userSettingsForm.new_password == '') {
+ i.state.userSettingsForm.new_password = undefined;
+ }
+ i.setState(i.state);
+ }
+
+ handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
+ i.state.userSettingsForm.new_password_verify = event.target.value;
+ if (i.state.userSettingsForm.new_password_verify == '') {
+ i.state.userSettingsForm.new_password_verify = undefined;
+ }
+ i.setState(i.state);
+ }
+
+ handleUserSettingsOldPasswordChange(i: User, event: any) {
+ i.state.userSettingsForm.old_password = event.target.value;
+ if (i.state.userSettingsForm.old_password == '') {
+ i.state.userSettingsForm.old_password = undefined;
+ }
+ i.setState(i.state);
+ }
+
handleImageUpload(i: User, event: any) {
event.preventDefault();
let file = event.target.files[0];
if (msg.error) {
alert(i18n.t(msg.error));
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('/');
}
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;
}
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
window.scrollTo(0, 0);
id: number;
name: string;
avatar?: string;
+ email?: string;
fedi_name: string;
published: string;
number_of_posts: number;
default_listing_type: ListingType;
lang: string;
avatar?: string;
+ email?: string;
+ new_password?: string;
+ new_password_verify?: string;
+ old_password?: string;
auth: string;
}
unread_messages: 'Unread Messages',
password: 'Password',
verify_password: 'Verify Password',
+ old_password: 'Old Password',
forgot_password: 'forgot password',
reset_password_mail_sent: 'Sent an Email to reset your password.',
password_change: 'Password Change',