footer.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. /* jshint ignore:start */
  2. import React from 'react';
  3. import * as actions from './controls/actions';
  4. import LikesModal from 'misago/components/post-likes';
  5. import modal from 'misago/services/modal';
  6. import posting from 'misago/services/posting';
  7. export default function(props) {
  8. if (!isVisible(props.post)) return null;
  9. return (
  10. <div className="post-footer">
  11. <MarkAsBestAnswer {...props} />
  12. <MarkAsBestAnswerCompact {...props} />
  13. <Like {...props} />
  14. <Likes
  15. lastLikes={props.post.last_likes}
  16. likes={props.post.likes}
  17. {...props}
  18. />
  19. <LikesCompact
  20. likes={props.post.likes}
  21. {...props}
  22. />
  23. <Reply {...props} />
  24. <Edit {...props} />
  25. </div>
  26. );
  27. }
  28. export function isVisible(post) {
  29. return (!post.is_hidden || post.acl.can_see_hidden) && (
  30. post.acl.can_reply ||
  31. post.acl.can_edit ||
  32. (post.acl.can_see_likes && (post.last_likes || []).length) ||
  33. post.acl.can_like
  34. );
  35. }
  36. export class MarkAsBestAnswer extends React.Component {
  37. onClick = () => {
  38. actions.markAsBestAnswer(this.props);
  39. };
  40. render() {
  41. const { post, thread } = this.props;
  42. if (!thread.acl.can_mark_best_answer) return null;
  43. if (!post.acl.can_mark_as_best_answer) return null;
  44. if (thread.best_answer && !thread.acl.can_change_best_answer) return null;
  45. return (
  46. <button
  47. className="hidden-xs btn btn-default btn-sm pull-left"
  48. disabled={this.props.post.isBusy || post.id === thread.best_answer}
  49. onClick={this.onClick}
  50. type="button"
  51. >
  52. <span className="material-icon">
  53. check_box
  54. </span>
  55. {gettext("Best answer")}
  56. </button>
  57. );
  58. }
  59. }
  60. export class MarkAsBestAnswerCompact extends React.Component {
  61. onClick = () => {
  62. actions.markAsBestAnswer(this.props);
  63. };
  64. render() {
  65. const { post, thread } = this.props;
  66. if (!thread.acl.can_mark_best_answer) return null;
  67. if (!post.acl.can_mark_as_best_answer) return null;
  68. if (thread.best_answer && !thread.acl.can_change_best_answer) return null;
  69. return (
  70. <button
  71. className="vixible-xs-inline-block btn btn-default btn-sm pull-left"
  72. disabled={this.props.post.isBusy || post.id === thread.best_answer}
  73. onClick={this.onClick}
  74. type="button"
  75. >
  76. <span className="material-icon">
  77. check_box
  78. </span>
  79. </button>
  80. );
  81. }
  82. }
  83. export class Like extends React.Component {
  84. onClick = () => {
  85. if (this.props.post.is_liked) {
  86. actions.unlike(this.props);
  87. } else {
  88. actions.like(this.props);
  89. }
  90. };
  91. render() {
  92. if (!this.props.post.acl.can_like) return null;
  93. let className = 'btn btn-default btn-sm pull-left';
  94. if (this.props.post.is_liked) {
  95. className = 'btn btn-success btn-sm pull-left';
  96. }
  97. return (
  98. <button
  99. className={className}
  100. disabled={this.props.post.isBusy}
  101. onClick={this.onClick}
  102. type="button"
  103. >
  104. {this.props.post.is_liked ? gettext("Liked") : gettext("Like")}
  105. </button>
  106. );
  107. }
  108. }
  109. export class Likes extends React.Component {
  110. onClick = () => {
  111. modal.show(
  112. <LikesModal
  113. post={this.props.post}
  114. />
  115. );
  116. };
  117. render() {
  118. const hasLikes = (this.props.post.last_likes || []).length > 0;
  119. if (!this.props.post.acl.can_see_likes || !hasLikes) return null;
  120. if (this.props.post.acl.can_see_likes === 2) {
  121. return (
  122. <button
  123. className="btn btn-link btn-sm pull-left hidden-xs"
  124. onClick={this.onClick}
  125. type="button"
  126. >
  127. {getLikesMessage(this.props.likes, this.props.lastLikes)}
  128. </button>
  129. );
  130. }
  131. return (
  132. <p className="pull-left hidden-xs">
  133. {getLikesMessage(this.props.likes, this.props.lastLikes)}
  134. </p>
  135. );
  136. }
  137. }
  138. export class LikesCompact extends Likes {
  139. render() {
  140. const hasLikes = (this.props.post.last_likes || []).length > 0;
  141. if (!this.props.post.acl.can_see_likes || !hasLikes) return null;
  142. if (this.props.post.acl.can_see_likes === 2) {
  143. return (
  144. <button
  145. className="btn btn-link btn-sm likes-compact pull-left visible-xs-block"
  146. onClick={this.onClick}
  147. type="button"
  148. >
  149. <span className="material-icon">
  150. favorite
  151. </span>
  152. {this.props.likes}
  153. </button>
  154. );
  155. }
  156. return (
  157. <p className="likes-compact pull-left visible-xs-block">
  158. <span className="material-icon">
  159. favorite
  160. </span>
  161. {this.props.likes}
  162. </p>
  163. );
  164. }
  165. }
  166. export function getLikesMessage(likes, users) {
  167. const usernames = users.slice(0, 3).map((u) => u.username);
  168. if (usernames.length == 1) {
  169. return interpolate(gettext("%(user)s likes this."), {
  170. user: usernames[0]
  171. }, true);
  172. }
  173. const hiddenLikes = likes - usernames.length;
  174. const otherUsers = usernames.slice(0, -1).join(', ');
  175. const lastUser = usernames.slice(-1)[0];
  176. const usernamesList = interpolate(gettext("%(users)s and %(last_user)s"), {
  177. users: otherUsers,
  178. last_user: lastUser
  179. }, true);
  180. if (hiddenLikes === 0) {
  181. return interpolate(gettext("%(users)s like this."), {
  182. users: usernamesList
  183. }, true);
  184. }
  185. const message = ngettext(
  186. "%(users)s and %(likes)s other user like this.",
  187. "%(users)s and %(likes)s other users like this.",
  188. hiddenLikes);
  189. return interpolate(message, {
  190. users: usernames.join(', '),
  191. likes: hiddenLikes
  192. }, true);
  193. }
  194. export class Reply extends React.Component {
  195. onClick = () => {
  196. posting.open({
  197. mode: 'REPLY',
  198. config: this.props.thread.api.editor,
  199. submit: this.props.thread.api.posts.index,
  200. context: {
  201. reply: this.props.post.id
  202. }
  203. });
  204. };
  205. render() {
  206. if (this.props.post.acl.can_reply) {
  207. return (
  208. <button
  209. className="btn btn-primary btn-sm pull-right"
  210. type="button"
  211. onClick={this.onClick}
  212. >
  213. {gettext("Reply")}
  214. </button>
  215. );
  216. } else {
  217. return null;
  218. }
  219. }
  220. }
  221. export class Edit extends React.Component {
  222. onClick = () => {
  223. posting.open({
  224. mode: 'EDIT',
  225. config: this.props.post.api.editor,
  226. submit: this.props.post.api.index
  227. });
  228. };
  229. render() {
  230. if (this.props.post.acl.can_edit) {
  231. return (
  232. <button
  233. className="hidden-xs btn btn-default btn-sm pull-right"
  234. type="button"
  235. onClick={this.onClick}
  236. >
  237. {gettext("Edit")}
  238. </button>
  239. );
  240. } else {
  241. return null;
  242. }
  243. }
  244. }