footer.js 6.4 KB

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