change-username.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import moment from 'moment';
  2. import React from 'react';
  3. import Avatar from 'misago/components/avatar'; // jshint ignore:line
  4. import Button from 'misago/components/button'; // jshint ignore:line
  5. import Form from 'misago/components/form';
  6. import FormGroup from 'misago/components/form-group'; // jshint ignore:line
  7. import Loader from 'misago/components/loader'; // jshint ignore:line
  8. import misago from 'misago/index';
  9. import { dehydrate, addNameChange } from 'misago/reducers/username-history'; // jshint ignore:line
  10. import { updateUsername } from 'misago/reducers/users'; // jshint ignore:line
  11. import ajax from 'misago/services/ajax';
  12. import snackbar from 'misago/services/snackbar';
  13. import store from 'misago/services/store';
  14. import * as random from 'misago/utils/random'; // jshint ignore:line
  15. import * as validators from 'misago/utils/validators';
  16. export class ChangeUsername extends Form {
  17. constructor(props) {
  18. super(props);
  19. this.state = {
  20. username: '',
  21. validators: {
  22. username: [
  23. validators.usernameContent(),
  24. validators.usernameMinLength({
  25. username_length_min: props.options.length_min
  26. }),
  27. validators.usernameMaxLength({
  28. username_length_max: props.options.length_max
  29. })
  30. ]
  31. },
  32. isLoading: false
  33. };
  34. }
  35. getHelpText() {
  36. let phrases = [];
  37. if (this.props.options.changes_left > 0) {
  38. let message = ngettext(
  39. "You can change your username %(changes_left)s more time.",
  40. "You can change your username %(changes_left)s more times.",
  41. this.props.options.changes_left);
  42. phrases.push(interpolate(message, {
  43. 'changes_left': this.props.options.changes_left
  44. }, true));
  45. }
  46. if (this.props.user.acl.name_changes_expire > 0) {
  47. let message = ngettext(
  48. "Used changes redeem after %(name_changes_expire)s day.",
  49. "Used changes redeem after %(name_changes_expire)s days.",
  50. this.props.user.acl.name_changes_expire);
  51. phrases.push(interpolate(message, {
  52. 'name_changes_expire': this.props.user.acl.name_changes_expire
  53. }, true));
  54. }
  55. return phrases.join(' ');
  56. }
  57. clean() {
  58. let errors = this.validate();
  59. if (errors.username) {
  60. snackbar.error(errors.username[0]);
  61. return false;
  62. } if (this.state.username.trim() === this.props.user.username) {
  63. snackbar.info(gettext("Your new username is same as current one."));
  64. return false;
  65. } else {
  66. return true;
  67. }
  68. }
  69. send() {
  70. return ajax.post(this.props.user.api_url.username, {
  71. 'username': this.state.username
  72. });
  73. }
  74. handleSuccess(success) {
  75. this.setState({
  76. 'username': ''
  77. });
  78. this.props.complete(success.username, success.slug, success.options);
  79. }
  80. handleError(rejection) {
  81. snackbar.error(rejection.detail);
  82. }
  83. render() {
  84. /* jshint ignore:start */
  85. return <form onSubmit={this.handleSubmit} className="form-horizontal">
  86. <div className="panel panel-default panel-form">
  87. <div className="panel-heading">
  88. <h3 className="panel-title">{gettext("Change username")}</h3>
  89. </div>
  90. <div className="panel-body">
  91. <FormGroup label={gettext("New username")} for="id_username"
  92. labelClass="col-sm-4" controlClass="col-sm-8"
  93. helpText={this.getHelpText()}>
  94. <input type="text" id="id_username" className="form-control"
  95. disabled={this.state.isLoading}
  96. onChange={this.bindInput('username')}
  97. value={this.state.username} />
  98. </FormGroup>
  99. </div>
  100. <div className="panel-footer">
  101. <div className="row">
  102. <div className="col-sm-8 col-sm-offset-4">
  103. <Button className="btn-primary" loading={this.state.isLoading}>
  104. {gettext("Change username")}
  105. </Button>
  106. </div>
  107. </div>
  108. </div>
  109. </div>
  110. </form>;
  111. /* jshint ignore:end */
  112. }
  113. }
  114. export class NoChangesLeft extends React.Component {
  115. getHelpText() {
  116. if (this.props.options.next_on) {
  117. return interpolate(
  118. gettext("You will be able to change your username %(next_change)s."),
  119. {'next_change': this.props.options.next_on.fromNow()}, true);
  120. } else {
  121. return gettext("You have used up available name changes.");
  122. }
  123. }
  124. render() {
  125. /* jshint ignore:start */
  126. return <div className="panel panel-default panel-form">
  127. <div className="panel-heading">
  128. <h3 className="panel-title">{gettext("Change username")}</h3>
  129. </div>
  130. <div className="panel-body panel-message-body">
  131. <div className="message-icon">
  132. <span className="material-icon">
  133. info_outline
  134. </span>
  135. </div>
  136. <div className="message-body">
  137. <p className="lead">
  138. {gettext("You can't change your username at the moment.")}
  139. </p>
  140. <p className="help-text">
  141. {this.getHelpText()}
  142. </p>
  143. </div>
  144. </div>
  145. </div>;
  146. /* jshint ignore:end */
  147. }
  148. }
  149. export class ChangeUsernameLoading extends React.Component {
  150. render() {
  151. /* jshint ignore:start */
  152. return <div className="panel panel-default panel-form">
  153. <div className="panel-heading">
  154. <h3 className="panel-title">{gettext("Change username")}</h3>
  155. </div>
  156. <div className="panel-body panel-body-loading">
  157. <Loader className="loader loader-spaced" />
  158. </div>
  159. </div>;
  160. /* jshint ignore:end */
  161. }
  162. }
  163. export class UsernameHistory extends React.Component {
  164. renderUserAvatar(item) {
  165. if (item.changed_by) {
  166. /* jshint ignore:start */
  167. return <a href={item.changed_by.absolute_url} className="user-avatar">
  168. <Avatar user={item.changed_by} size="100" />
  169. </a>;
  170. /* jshint ignore:end */
  171. } else {
  172. /* jshint ignore:start */
  173. return <span className="user-avatar">
  174. <Avatar size="100" />
  175. </span>;
  176. /* jshint ignore:end */
  177. }
  178. }
  179. renderUsername(item) {
  180. if (item.changed_by) {
  181. /* jshint ignore:start */
  182. return <a href={item.changed_by.absolute_url} className="item-title">
  183. {item.changed_by.username}
  184. </a>;
  185. /* jshint ignore:end */
  186. } else {
  187. /* jshint ignore:start */
  188. return <span className="item-title">
  189. {item.changed_by_username}
  190. </span>;
  191. /* jshint ignore:end */
  192. }
  193. }
  194. renderHistory() {
  195. /* jshint ignore:start */
  196. return <div className="username-history ui-ready">
  197. <ul className="list-group">
  198. {this.props.changes.map((item, i) => {
  199. return <li className="list-group-item" key={i}>
  200. <div className="username-change-avatar">
  201. {this.renderUserAvatar(item)}
  202. </div>
  203. <div className="username-change-author">
  204. {this.renderUsername(item)}
  205. </div>
  206. <div className="username-change">
  207. {item.old_username}
  208. <span className="material-icon">
  209. arrow_forward
  210. </span>
  211. {item.new_username}
  212. </div>
  213. <div className="username-change-date">
  214. <abbr title={item.changed_on.format('LLL')}>
  215. {item.changed_on.fromNow()}
  216. </abbr>
  217. </div>
  218. </li>;
  219. })}
  220. </ul>
  221. </div>;
  222. /* jshint ignore:end */
  223. }
  224. renderEmptyHistory() {
  225. /* jshint ignore:start */
  226. return <div className="username-history ui-ready">
  227. <ul className="list-group">
  228. <li className="list-group-item empty-message">
  229. {gettext("No name changes have been recorded for your account.")}
  230. </li>
  231. </ul>
  232. </div>;
  233. /* jshint ignore:end */
  234. }
  235. renderHistoryPreview() {
  236. /* jshint ignore:start */
  237. return <div className="username-history ui-preview">
  238. <ul className="list-group">
  239. {random.range(3, 5).map((i) => {
  240. return <li className="list-group-item" key={i}>
  241. <div className="username-change-avatar">
  242. <span className="user-avatar">
  243. <Avatar size="100" />
  244. </span>
  245. </div>
  246. <div className="username-change-author">
  247. <span className="ui-preview-text" style={{width: random.int(30, 100) + "px"}}>&nbsp;</span>
  248. </div>
  249. <div className="username-change">
  250. <span className="ui-preview-text" style={{width: random.int(30, 50) + "px"}}>&nbsp;</span>
  251. <span className="material-icon">
  252. arrow_forward
  253. </span>
  254. <span className="ui-preview-text" style={{width: random.int(30, 50) + "px"}}>&nbsp;</span>
  255. </div>
  256. <div className="username-change-date">
  257. <span className="ui-preview-text" style={{width: random.int(50, 100) + "px"}}>&nbsp;</span>
  258. </div>
  259. </li>
  260. })}
  261. </ul>
  262. </div>;
  263. /* jshint ignore:end */
  264. }
  265. render() {
  266. if (this.props.isLoaded) {
  267. if (this.props.changes.length) {
  268. return this.renderHistory();
  269. } else {
  270. return this.renderEmptyHistory();
  271. }
  272. } else {
  273. return this.renderHistoryPreview();
  274. }
  275. }
  276. }
  277. export default class extends React.Component {
  278. constructor(props) {
  279. super(props);
  280. this.state = {
  281. isLoaded: false,
  282. options: null
  283. };
  284. }
  285. componentDidMount() {
  286. Promise.all([
  287. ajax.get(this.props.user.api_url.username),
  288. ajax.get(misago.get('USERNAME_CHANGES_API'), {user: this.props.user.id})
  289. ]).then((data) => {
  290. this.setState({
  291. isLoaded: true,
  292. options: {
  293. changes_left: data[0].changes_left,
  294. length_min: data[0].length_min,
  295. length_max: data[0].length_max,
  296. next_on: data[0].next_on ? moment(data[0].next_on) : null,
  297. }
  298. });
  299. store.dispatch(dehydrate(data[1].results));
  300. });
  301. }
  302. /* jshint ignore:start */
  303. onComplete = (username, slug, options) => {
  304. this.setState({
  305. options
  306. });
  307. store.dispatch(
  308. addNameChange({ username, slug }, this.props.user, this.props.user));
  309. store.dispatch(
  310. updateUsername(this.props.user, username, slug));
  311. snackbar.success(gettext("Your username has been changed successfully."));
  312. };
  313. /* jshint ignore:end */
  314. getChangeForm() {
  315. if (this.state.isLoaded) {
  316. if (this.state.options.changes_left > 0) {
  317. /* jshint ignore:start */
  318. return <ChangeUsername user={this.props.user}
  319. options={this.state.options}
  320. complete={this.onComplete} />;
  321. /* jshint ignore:end */
  322. } else {
  323. /* jshint ignore:start */
  324. return <NoChangesLeft options={this.state.options} />;
  325. /* jshint ignore:end */
  326. }
  327. } else {
  328. /* jshint ignore:start */
  329. return <ChangeUsernameLoading />;
  330. /* jshint ignore:end */
  331. }
  332. }
  333. render() {
  334. /* jshint ignore:start */
  335. return <div>
  336. {this.getChangeForm()}
  337. <UsernameHistory isLoaded={this.state.isLoaded}
  338. changes={this.props['username-history']} />
  339. </div>
  340. /* jshint ignore:end */
  341. }
  342. }