The code powering m.abunchtell.com https://m.abunchtell.com
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 

300 rindas
10 KiB

  1. import React from 'react';
  2. import ImmutablePropTypes from 'react-immutable-proptypes';
  3. import PropTypes from 'prop-types';
  4. import Avatar from './avatar';
  5. import AvatarOverlay from './avatar_overlay';
  6. import RelativeTimestamp from './relative_timestamp';
  7. import DisplayName from './display_name';
  8. import StatusContent from './status_content';
  9. import StatusActionBar from './status_action_bar';
  10. import AttachmentList from './attachment_list';
  11. import { injectIntl, FormattedMessage } from 'react-intl';
  12. import ImmutablePureComponent from 'react-immutable-pure-component';
  13. import { MediaGallery, Video } from '../features/ui/util/async-components';
  14. import { HotKeys } from 'react-hotkeys';
  15. import classNames from 'classnames';
  16. // We use the component (and not the container) since we do not want
  17. // to use the progress bar to show download progress
  18. import Bundle from '../features/ui/components/bundle';
  19. export const textForScreenReader = (intl, status, rebloggedByText = false, expanded = false) => {
  20. const displayName = status.getIn(['account', 'display_name']);
  21. const values = [
  22. displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
  23. status.get('spoiler_text') && !expanded ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length),
  24. intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
  25. status.getIn(['account', 'acct']),
  26. ];
  27. if (rebloggedByText) {
  28. values.push(rebloggedByText);
  29. }
  30. return values.join(', ');
  31. };
  32. @injectIntl
  33. export default class Status extends ImmutablePureComponent {
  34. static contextTypes = {
  35. router: PropTypes.object,
  36. };
  37. static propTypes = {
  38. status: ImmutablePropTypes.map,
  39. account: ImmutablePropTypes.map,
  40. onReply: PropTypes.func,
  41. onFavourite: PropTypes.func,
  42. onReblog: PropTypes.func,
  43. onDelete: PropTypes.func,
  44. onDirect: PropTypes.func,
  45. onMention: PropTypes.func,
  46. onPin: PropTypes.func,
  47. onOpenMedia: PropTypes.func,
  48. onOpenVideo: PropTypes.func,
  49. onBlock: PropTypes.func,
  50. onEmbed: PropTypes.func,
  51. onHeightChange: PropTypes.func,
  52. onToggleHidden: PropTypes.func,
  53. muted: PropTypes.bool,
  54. hidden: PropTypes.bool,
  55. onMoveUp: PropTypes.func,
  56. onMoveDown: PropTypes.func,
  57. };
  58. // Avoid checking props that are functions (and whose equality will always
  59. // evaluate to false. See react-immutable-pure-component for usage.
  60. updateOnProps = [
  61. 'status',
  62. 'account',
  63. 'muted',
  64. 'hidden',
  65. ]
  66. handleClick = () => {
  67. if (!this.context.router) {
  68. return;
  69. }
  70. const { status } = this.props;
  71. this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
  72. }
  73. handleAccountClick = (e) => {
  74. if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
  75. const id = e.currentTarget.getAttribute('data-id');
  76. e.preventDefault();
  77. this.context.router.history.push(`/accounts/${id}`);
  78. }
  79. }
  80. handleExpandedToggle = () => {
  81. this.props.onToggleHidden(this._properStatus());
  82. };
  83. renderLoadingMediaGallery () {
  84. return <div className='media_gallery' style={{ height: '110px' }} />;
  85. }
  86. renderLoadingVideoPlayer () {
  87. return <div className='media-spoiler-video' style={{ height: '110px' }} />;
  88. }
  89. handleOpenVideo = (media, startTime) => {
  90. this.props.onOpenVideo(media, startTime);
  91. }
  92. handleHotkeyReply = e => {
  93. e.preventDefault();
  94. this.props.onReply(this._properStatus(), this.context.router.history);
  95. }
  96. handleHotkeyFavourite = () => {
  97. this.props.onFavourite(this._properStatus());
  98. }
  99. handleHotkeyBoost = e => {
  100. this.props.onReblog(this._properStatus(), e);
  101. }
  102. handleHotkeyMention = e => {
  103. e.preventDefault();
  104. this.props.onMention(this._properStatus().get('account'), this.context.router.history);
  105. }
  106. handleHotkeyOpen = () => {
  107. this.context.router.history.push(`/statuses/${this._properStatus().get('id')}`);
  108. }
  109. handleHotkeyOpenProfile = () => {
  110. this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`);
  111. }
  112. handleHotkeyMoveUp = e => {
  113. this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
  114. }
  115. handleHotkeyMoveDown = e => {
  116. this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
  117. }
  118. handleHotkeyToggleHidden = () => {
  119. this.props.onToggleHidden(this._properStatus());
  120. }
  121. _properStatus () {
  122. const { status } = this.props;
  123. if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
  124. return status.get('reblog');
  125. } else {
  126. return status;
  127. }
  128. }
  129. render () {
  130. let media = null;
  131. let statusAvatar, prepend, rebloggedByText;
  132. const { intl, hidden, featured } = this.props;
  133. let { status, account, ...other } = this.props;
  134. if (status === null) {
  135. return null;
  136. }
  137. if (hidden) {
  138. return (
  139. <div>
  140. {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
  141. {status.get('content')}
  142. </div>
  143. );
  144. }
  145. if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) {
  146. const minHandlers = this.props.muted ? {} : {
  147. moveUp: this.handleHotkeyMoveUp,
  148. moveDown: this.handleHotkeyMoveDown,
  149. };
  150. return (
  151. <HotKeys handlers={minHandlers}>
  152. <div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0'>
  153. <FormattedMessage id='status.filtered' defaultMessage='Filtered' />
  154. </div>
  155. </HotKeys>
  156. );
  157. }
  158. if (featured) {
  159. prepend = (
  160. <div className='status__prepend'>
  161. <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-thumb-tack status__prepend-icon' /></div>
  162. <FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
  163. </div>
  164. );
  165. } else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
  166. const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
  167. prepend = (
  168. <div className='status__prepend'>
  169. <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
  170. <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
  171. </div>
  172. );
  173. rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: status.getIn(['account', 'acct']) });
  174. account = status.get('account');
  175. status = status.get('reblog');
  176. }
  177. if (status.get('media_attachments').size > 0) {
  178. if (this.props.muted || status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
  179. media = (
  180. <AttachmentList
  181. compact
  182. media={status.get('media_attachments')}
  183. />
  184. );
  185. } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
  186. const video = status.getIn(['media_attachments', 0]);
  187. media = (
  188. <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
  189. {Component => (
  190. <Component
  191. preview={video.get('preview_url')}
  192. src={video.get('url')}
  193. alt={video.get('description')}
  194. width={239}
  195. height={110}
  196. inline
  197. sensitive={status.get('sensitive')}
  198. onOpenVideo={this.handleOpenVideo}
  199. />
  200. )}
  201. </Bundle>
  202. );
  203. } else {
  204. media = (
  205. <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
  206. {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />}
  207. </Bundle>
  208. );
  209. }
  210. }
  211. if (account === undefined || account === null) {
  212. statusAvatar = <Avatar account={status.get('account')} size={48} />;
  213. }else{
  214. statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
  215. }
  216. const handlers = this.props.muted ? {} : {
  217. reply: this.handleHotkeyReply,
  218. favourite: this.handleHotkeyFavourite,
  219. boost: this.handleHotkeyBoost,
  220. mention: this.handleHotkeyMention,
  221. open: this.handleHotkeyOpen,
  222. openProfile: this.handleHotkeyOpenProfile,
  223. moveUp: this.handleHotkeyMoveUp,
  224. moveDown: this.handleHotkeyMoveDown,
  225. toggleHidden: this.handleHotkeyToggleHidden,
  226. };
  227. return (
  228. <HotKeys handlers={handlers}>
  229. <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}>
  230. {prepend}
  231. <div className={classNames('status', `status-${status.get('visibility')}`, { muted: this.props.muted })} data-id={status.get('id')}>
  232. <div className='status__info'>
  233. <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
  234. <a onClick={this.handleAccountClick} target='_blank' data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name'>
  235. <div className='status__avatar'>
  236. {statusAvatar}
  237. </div>
  238. <DisplayName account={status.get('account')} />
  239. </a>
  240. </div>
  241. <StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
  242. {media}
  243. <StatusActionBar status={status} account={account} {...other} />
  244. </div>
  245. </div>
  246. </HotKeys>
  247. );
  248. }
  249. }