The code powering m.abunchtell.com https://m.abunchtell.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

320 lines
8.6 KiB

  1. import React from 'react';
  2. import { connect } from 'react-redux';
  3. import PropTypes from 'prop-types';
  4. import ImmutablePropTypes from 'react-immutable-proptypes';
  5. import { fetchStatus } from '../../actions/statuses';
  6. import MissingIndicator from '../../components/missing_indicator';
  7. import DetailedStatus from './components/detailed_status';
  8. import ActionBar from './components/action_bar';
  9. import Column from '../ui/components/column';
  10. import {
  11. favourite,
  12. unfavourite,
  13. reblog,
  14. unreblog,
  15. pin,
  16. unpin,
  17. } from '../../actions/interactions';
  18. import {
  19. replyCompose,
  20. mentionCompose,
  21. } from '../../actions/compose';
  22. import { deleteStatus } from '../../actions/statuses';
  23. import { initReport } from '../../actions/reports';
  24. import { makeGetStatus } from '../../selectors';
  25. import { ScrollContainer } from 'react-router-scroll-4';
  26. import ColumnBackButton from '../../components/column_back_button';
  27. import StatusContainer from '../../containers/status_container';
  28. import { openModal } from '../../actions/modal';
  29. import { defineMessages, injectIntl } from 'react-intl';
  30. import ImmutablePureComponent from 'react-immutable-pure-component';
  31. import { HotKeys } from 'react-hotkeys';
  32. import { boostModal, deleteModal } from '../../initial_state';
  33. const messages = defineMessages({
  34. deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
  35. deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
  36. });
  37. const makeMapStateToProps = () => {
  38. const getStatus = makeGetStatus();
  39. const mapStateToProps = (state, props) => ({
  40. status: getStatus(state, props.params.statusId),
  41. ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]),
  42. descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]),
  43. });
  44. return mapStateToProps;
  45. };
  46. @injectIntl
  47. @connect(makeMapStateToProps)
  48. export default class Status extends ImmutablePureComponent {
  49. static contextTypes = {
  50. router: PropTypes.object,
  51. };
  52. static propTypes = {
  53. params: PropTypes.object.isRequired,
  54. dispatch: PropTypes.func.isRequired,
  55. status: ImmutablePropTypes.map,
  56. ancestorsIds: ImmutablePropTypes.list,
  57. descendantsIds: ImmutablePropTypes.list,
  58. intl: PropTypes.object.isRequired,
  59. };
  60. componentWillMount () {
  61. this.props.dispatch(fetchStatus(this.props.params.statusId));
  62. }
  63. componentWillReceiveProps (nextProps) {
  64. if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
  65. this._scrolledIntoView = false;
  66. this.props.dispatch(fetchStatus(nextProps.params.statusId));
  67. }
  68. }
  69. handleFavouriteClick = (status) => {
  70. if (status.get('favourited')) {
  71. this.props.dispatch(unfavourite(status));
  72. } else {
  73. this.props.dispatch(favourite(status));
  74. }
  75. }
  76. handlePin = (status) => {
  77. if (status.get('pinned')) {
  78. this.props.dispatch(unpin(status));
  79. } else {
  80. this.props.dispatch(pin(status));
  81. }
  82. }
  83. handleReplyClick = (status) => {
  84. this.props.dispatch(replyCompose(status, this.context.router.history));
  85. }
  86. handleModalReblog = (status) => {
  87. this.props.dispatch(reblog(status));
  88. }
  89. handleReblogClick = (status, e) => {
  90. if (status.get('reblogged')) {
  91. this.props.dispatch(unreblog(status));
  92. } else {
  93. if (e.shiftKey || !boostModal) {
  94. this.handleModalReblog(status);
  95. } else {
  96. this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
  97. }
  98. }
  99. }
  100. handleDeleteClick = (status) => {
  101. const { dispatch, intl } = this.props;
  102. if (!deleteModal) {
  103. dispatch(deleteStatus(status.get('id')));
  104. } else {
  105. dispatch(openModal('CONFIRM', {
  106. message: intl.formatMessage(messages.deleteMessage),
  107. confirm: intl.formatMessage(messages.deleteConfirm),
  108. onConfirm: () => dispatch(deleteStatus(status.get('id'))),
  109. }));
  110. }
  111. }
  112. handleMentionClick = (account, router) => {
  113. this.props.dispatch(mentionCompose(account, router));
  114. }
  115. handleOpenMedia = (media, index) => {
  116. this.props.dispatch(openModal('MEDIA', { media, index }));
  117. }
  118. handleOpenVideo = (media, time) => {
  119. this.props.dispatch(openModal('VIDEO', { media, time }));
  120. }
  121. handleReport = (status) => {
  122. this.props.dispatch(initReport(status.get('account'), status));
  123. }
  124. handleEmbed = (status) => {
  125. this.props.dispatch(openModal('EMBED', { url: status.get('url') }));
  126. }
  127. handleHotkeyMoveUp = () => {
  128. this.handleMoveUp(this.props.status.get('id'));
  129. }
  130. handleHotkeyMoveDown = () => {
  131. this.handleMoveDown(this.props.status.get('id'));
  132. }
  133. handleHotkeyReply = e => {
  134. e.preventDefault();
  135. this.handleReplyClick(this.props.status);
  136. }
  137. handleHotkeyFavourite = () => {
  138. this.handleFavouriteClick(this.props.status);
  139. }
  140. handleHotkeyBoost = () => {
  141. this.handleReblogClick(this.props.status);
  142. }
  143. handleHotkeyMention = e => {
  144. e.preventDefault();
  145. this.handleMentionClick(this.props.status);
  146. }
  147. handleHotkeyOpenProfile = () => {
  148. this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
  149. }
  150. handleMoveUp = id => {
  151. const { status, ancestorsIds, descendantsIds } = this.props;
  152. if (id === status.get('id')) {
  153. this._selectChild(ancestorsIds.size - 1);
  154. } else {
  155. let index = ancestorsIds.indexOf(id);
  156. if (index === -1) {
  157. index = descendantsIds.indexOf(id);
  158. this._selectChild(ancestorsIds.size + index);
  159. } else {
  160. this._selectChild(index - 1);
  161. }
  162. }
  163. }
  164. handleMoveDown = id => {
  165. const { status, ancestorsIds, descendantsIds } = this.props;
  166. if (id === status.get('id')) {
  167. this._selectChild(ancestorsIds.size + 1);
  168. } else {
  169. let index = ancestorsIds.indexOf(id);
  170. if (index === -1) {
  171. index = descendantsIds.indexOf(id);
  172. this._selectChild(ancestorsIds.size + index + 2);
  173. } else {
  174. this._selectChild(index + 1);
  175. }
  176. }
  177. }
  178. _selectChild (index) {
  179. const element = this.node.querySelectorAll('.focusable')[index];
  180. if (element) {
  181. element.focus();
  182. }
  183. }
  184. renderChildren (list) {
  185. return list.map(id => (
  186. <StatusContainer
  187. key={id}
  188. id={id}
  189. onMoveUp={this.handleMoveUp}
  190. onMoveDown={this.handleMoveDown}
  191. />
  192. ));
  193. }
  194. setRef = c => {
  195. this.node = c;
  196. }
  197. componentDidUpdate () {
  198. if (this._scrolledIntoView) {
  199. return;
  200. }
  201. const { status, ancestorsIds } = this.props;
  202. if (status && ancestorsIds && ancestorsIds.size > 0) {
  203. const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size - 1];
  204. element.scrollIntoView(true);
  205. this._scrolledIntoView = true;
  206. }
  207. }
  208. render () {
  209. let ancestors, descendants;
  210. const { status, ancestorsIds, descendantsIds } = this.props;
  211. if (status === null) {
  212. return (
  213. <Column>
  214. <ColumnBackButton />
  215. <MissingIndicator />
  216. </Column>
  217. );
  218. }
  219. if (ancestorsIds && ancestorsIds.size > 0) {
  220. ancestors = <div>{this.renderChildren(ancestorsIds)}</div>;
  221. }
  222. if (descendantsIds && descendantsIds.size > 0) {
  223. descendants = <div>{this.renderChildren(descendantsIds)}</div>;
  224. }
  225. const handlers = {
  226. moveUp: this.handleHotkeyMoveUp,
  227. moveDown: this.handleHotkeyMoveDown,
  228. reply: this.handleHotkeyReply,
  229. favourite: this.handleHotkeyFavourite,
  230. boost: this.handleHotkeyBoost,
  231. mention: this.handleHotkeyMention,
  232. openProfile: this.handleHotkeyOpenProfile,
  233. };
  234. return (
  235. <Column>
  236. <ColumnBackButton />
  237. <ScrollContainer scrollKey='thread'>
  238. <div className='scrollable detailed-status__wrapper' ref={this.setRef}>
  239. {ancestors}
  240. <HotKeys handlers={handlers}>
  241. <div className='focusable' tabIndex='0'>
  242. <DetailedStatus
  243. status={status}
  244. onOpenVideo={this.handleOpenVideo}
  245. onOpenMedia={this.handleOpenMedia}
  246. />
  247. <ActionBar
  248. status={status}
  249. onReply={this.handleReplyClick}
  250. onFavourite={this.handleFavouriteClick}
  251. onReblog={this.handleReblogClick}
  252. onDelete={this.handleDeleteClick}
  253. onMention={this.handleMentionClick}
  254. onReport={this.handleReport}
  255. onPin={this.handlePin}
  256. onEmbed={this.handleEmbed}
  257. />
  258. </div>
  259. </HotKeys>
  260. {descendants}
  261. </div>
  262. </ScrollContainer>
  263. </Column>
  264. );
  265. }
  266. }