The code powering m.abunchtell.com https://m.abunchtell.com
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 

339 rader
9.1 KiB

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