thread.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import React from 'react';
  2. import { Link } from 'react-router'; // jshint ignore:line
  3. import ReadIcon from 'misago/components/threads-list/read-icon'; // jshint ignore:line
  4. import ThreadOptions from 'misago/components/threads-list/thread-options'; // jshint ignore:line
  5. import escapeHtml from 'misago/utils/escape-html';
  6. const LAST_POSTER_URL = '<a href="%(url)s" class="poster-title">%(user)s</a>';
  7. const LAST_POSTER_SPAN = '<span class="poster-title">%(user)s</span>';
  8. const LAST_REPLY_URL = '<a href="%(url)s" class="last-title" title="%(absolute)s">%(relative)s</a>';
  9. export class Category extends React.Component {
  10. getClassName() {
  11. if (this.props.category.css_class) {
  12. return 'thread-category thread-category-' + this.props.category.css_class;
  13. } else {
  14. return 'thread-category';
  15. }
  16. }
  17. getUrl() {
  18. return this.props.category.absolute_url + this.props.list.path;
  19. }
  20. render() {
  21. /* jshint ignore:start */
  22. return <Link to={this.getUrl()} className={this.getClassName()}>
  23. {this.props.category.name}
  24. </Link>;
  25. /* jshint ignore:end */
  26. }
  27. }
  28. export default class extends React.Component {
  29. constructor(props) {
  30. super(props);
  31. this.state = {
  32. isSelected: false
  33. };
  34. }
  35. getPath() {
  36. let top = this.props.categories[this.props.thread.top_category];
  37. let bottom = this.props.categories[this.props.thread.category];
  38. if (top && bottom && top.id !== bottom.id) {
  39. /* jshint ignore:start */
  40. return <li className="thread-path">
  41. <Category category={top} list={this.props.list} />
  42. <span className="path-separator material-icon">
  43. arrow_forward
  44. </span>
  45. <Category category={bottom} list={this.props.list} />
  46. </li>;
  47. /* jshint ignore:end */
  48. } else if (top || bottom) {
  49. /* jshint ignore:start */
  50. return <li className="thread-path">
  51. <Category category={top || bottom} list={this.props.list} />
  52. </li>;
  53. /* jshint ignore:end */
  54. } else {
  55. return null;
  56. }
  57. }
  58. getClosedLabel() {
  59. if (this.props.thread.is_closed) {
  60. /* jshint ignore:start */
  61. return <li className="thread-closed">
  62. {gettext("Closed")}
  63. </li>;
  64. /* jshint ignore:end */
  65. } else {
  66. return null;
  67. }
  68. }
  69. getNewLabel() {
  70. if (!this.props.thread.is_read) {
  71. /* jshint ignore:start */
  72. return <li className="thread-new-posts"
  73. title={gettext("Go to first unread post")}>
  74. <a href={this.props.thread.new_post_url}>
  75. {gettext("New posts")}
  76. </a>
  77. </li>;
  78. /* jshint ignore:end */
  79. } else {
  80. return null;
  81. }
  82. }
  83. getRepliesCount() {
  84. /* jshint ignore:start */
  85. let message = ngettext(
  86. "%(replies)s reply",
  87. "%(replies)s replies",
  88. this.props.thread.replies);
  89. return <li className="thread-replies">
  90. <a href={this.props.thread.absolute_url}>
  91. {interpolate(message, {
  92. replies: this.props.thread.replies,
  93. }, true)}
  94. </a>
  95. </li>;
  96. /* jshint ignore:end */
  97. }
  98. getLastReplyDate() {
  99. return interpolate(LAST_REPLY_URL, {
  100. url: escapeHtml(this.props.thread.last_post_url),
  101. absolute: escapeHtml(this.props.thread.last_post_on.format('LLL')),
  102. relative: escapeHtml(this.props.thread.last_post_on.fromNow())
  103. }, true);
  104. }
  105. getLastPoster() {
  106. if (this.props.thread.last_poster_url) {
  107. return interpolate(LAST_POSTER_URL, {
  108. url: escapeHtml(this.props.thread.last_poster_url),
  109. user: escapeHtml(this.props.thread.last_poster_name)
  110. }, true);
  111. } else {
  112. return interpolate(LAST_POSTER_SPAN, {
  113. user: escapeHtml(this.props.thread.last_poster_name)
  114. }, true);
  115. }
  116. }
  117. getLastReply() {
  118. /* jshint ignore:start */
  119. return <li className="thread-last-reply"
  120. dangerouslySetInnerHTML={{__html: interpolate(
  121. escapeHtml(gettext("last reply by %(user)s %(date)s")), {
  122. date: this.getLastReplyDate(),
  123. user: this.getLastPoster()
  124. }, true)}} />;
  125. /* jshint ignore:end */
  126. }
  127. getOptions() {
  128. if (this.props.user.id) {
  129. /* jshint ignore:start */
  130. return <ThreadOptions thread={this.props.thread}
  131. selectThread={this.props.selectThread}
  132. isSelected={this.props.isSelected} />;
  133. /* jshint ignore:end */
  134. } else {
  135. return null;
  136. }
  137. }
  138. getClassName() {
  139. if (this.props.thread.is_read) {
  140. if (this.props.isSelected) {
  141. return 'list-group-item thread-read thread-selected';
  142. } else {
  143. return 'list-group-item thread-read';
  144. }
  145. } else {
  146. if (this.props.isSelected) {
  147. return 'list-group-item thread-new thread-selected';
  148. } else {
  149. return 'list-group-item thread-new';
  150. }
  151. }
  152. }
  153. render () {
  154. /* jshint ignore:start */
  155. return <li className={this.getClassName()}>
  156. <ReadIcon thread={this.props.thread} />
  157. <div className="thread-main">
  158. <a href={this.props.thread.absolute_url} className="item-title thread-title">
  159. {this.props.thread.title}
  160. </a>
  161. <ul className="list-inline">
  162. {this.getNewLabel()}
  163. {this.getClosedLabel()}
  164. {this.getPath()}
  165. {this.getRepliesCount()}
  166. {this.getLastReply()}
  167. </ul>
  168. </div>
  169. {this.getOptions()}
  170. </li>;
  171. /* jshint ignore:end */
  172. }
  173. }