about summary refs log tree commit diff
path: root/app/javascript/glitch/components/status
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/glitch/components/status')
-rw-r--r--app/javascript/glitch/components/status/action_bar.js58
-rw-r--r--app/javascript/glitch/components/status/container.js30
-rw-r--r--app/javascript/glitch/components/status/content.js20
-rw-r--r--app/javascript/glitch/components/status/gallery/item.js20
-rw-r--r--app/javascript/glitch/components/status/header.js166
-rw-r--r--app/javascript/glitch/components/status/index.js7
-rw-r--r--app/javascript/glitch/components/status/prepend.js8
7 files changed, 130 insertions, 179 deletions
diff --git a/app/javascript/glitch/components/status/action_bar.js b/app/javascript/glitch/components/status/action_bar.js
index 7c73002c1..f4450d31b 100644
--- a/app/javascript/glitch/components/status/action_bar.js
+++ b/app/javascript/glitch/components/status/action_bar.js
@@ -8,7 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 //  Mastodon imports  //
 import RelativeTimestamp from '../../../mastodon/components/relative_timestamp';
 import IconButton from '../../../mastodon/components/icon_button';
-import DropdownMenu from '../../../mastodon/components/dropdown_menu';
+import DropdownMenuContainer from '../../../mastodon/containers/dropdown_menu_container';
 
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -16,6 +16,7 @@ const messages = defineMessages({
   mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
   block: { id: 'account.block', defaultMessage: 'Block @{name}' },
   reply: { id: 'status.reply', defaultMessage: 'Reply' },
+  share: { id: 'status.share', defaultMessage: 'Share' },
   replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
   reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
   cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
@@ -24,6 +25,9 @@ const messages = defineMessages({
   report: { id: 'status.report', defaultMessage: 'Report @{name}' },
   muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
   unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
+  pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
+  unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
+  embed: { id: 'status.embed', defaultMessage: 'Embed' },
 });
 
 @injectIntl
@@ -43,8 +47,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
     onMute: PropTypes.func,
     onBlock: PropTypes.func,
     onReport: PropTypes.func,
+    onEmbed: PropTypes.func,
     onMuteConversation: PropTypes.func,
-    me: PropTypes.number,
+    onPin: PropTypes.func,
+    me: PropTypes.string,
     withDismiss: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
@@ -61,6 +67,13 @@ export default class StatusActionBar extends ImmutablePureComponent {
     this.props.onReply(this.props.status, this.context.router.history);
   }
 
+  handleShareClick = () => {
+    navigator.share({
+      text: this.props.status.get('search_index'),
+      url: this.props.status.get('url'),
+    });
+  }
+
   handleFavouriteClick = () => {
     this.props.onFavourite(this.props.status);
   }
@@ -73,6 +86,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
     this.props.onDelete(this.props.status);
   }
 
+  handlePinClick = () => {
+    this.props.onPin(this.props.status);
+  }
+
   handleMentionClick = () => {
     this.props.onMention(this.props.status.get('account'), this.context.router.history);
   }
@@ -89,6 +106,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
     this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
   }
 
+  handleEmbed = () => {
+    this.props.onEmbed(this.props.status);
+  }
+
   handleReport = () => {
     this.props.onReport(this.props.status);
   }
@@ -99,9 +120,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
 
   render () {
     const { status, me, intl, withDismiss } = this.props;
-    const reblogDisabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct';
+
     const mutingConversation = status.get('muted');
     const anonymousAccess = !me;
+    const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility'));
 
     let menu = [];
     let reblogIcon = 'retweet';
@@ -109,14 +131,23 @@ export default class StatusActionBar extends ImmutablePureComponent {
     let replyTitle;
 
     menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
+
+    if (publicStatus) {
+      menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
+    }
+
     menu.push(null);
 
-    if (withDismiss) {
+    if (status.getIn(['account', 'id']) === me || withDismiss) {
       menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
       menu.push(null);
     }
 
     if (status.getIn(['account', 'id']) === me) {
+      if (publicStatus) {
+        menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
+      }
+
       menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
     } else {
       menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
@@ -126,14 +157,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
       menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
     }
 
-    /*
-    if (status.get('visibility') === 'direct') {
-      reblogIcon = 'envelope';
-    } else if (status.get('visibility') === 'private') {
-      reblogIcon = 'lock';
-    }
-    */
-
     if (status.get('in_reply_to_id', null) === null) {
       replyIcon = 'reply';
       replyTitle = intl.formatMessage(messages.reply);
@@ -142,14 +165,19 @@ export default class StatusActionBar extends ImmutablePureComponent {
       replyTitle = intl.formatMessage(messages.replyAll);
     }
 
+    const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
+      <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
+    );
+
     return (
       <div className='status__action-bar'>
         <IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
-        <IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
-        <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
+        <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
+        <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
+        {shareButton}
 
         <div className='status__action-bar-dropdown'>
-          <DropdownMenu items={menu} disabled={anonymousAccess} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
+          <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
         </div>
 
         <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
diff --git a/app/javascript/glitch/components/status/container.js b/app/javascript/glitch/components/status/container.js
index 1d572e0e7..da2771c0b 100644
--- a/app/javascript/glitch/components/status/container.js
+++ b/app/javascript/glitch/components/status/container.js
@@ -38,11 +38,11 @@ import {
   favourite,
   unreblog,
   unfavourite,
+  pin,
+  unpin,
 } from '../../../mastodon/actions/interactions';
-import {
-  blockAccount,
-  muteAccount,
-} from '../../../mastodon/actions/accounts';
+import { blockAccount } from '../../../mastodon/actions/accounts';
+import { initMuteModal } from '../../../mastodon/actions/mutes';
 import {
   muteStatus,
   unmuteStatus,
@@ -80,10 +80,6 @@ const messages = defineMessages({
     id             : 'confirmations.block.confirm',
     defaultMessage : 'Block',
   },
-  muteConfirm : {
-    id             : 'confirmations.mute.confirm',
-    defaultMessage : 'Mute',
-  },
 });
 
                             /* * * * */
@@ -193,6 +189,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     }
   },
 
+  onPin (status) {
+    if (status.get('pinned')) {
+      dispatch(unpin(status));
+    } else {
+      dispatch(pin(status));
+    }
+  },
+
+  onEmbed (status) {
+    dispatch(openModal('EMBED', { url: status.get('url') }));
+  },
+
   onDelete (status) {
     if (!this.deleteModal) {
       dispatch(deleteStatus(status.get('id')));
@@ -230,11 +238,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   },
 
   onMute (account) {
-    dispatch(openModal('CONFIRM', {
-      message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
-      confirm: intl.formatMessage(messages.muteConfirm),
-      onConfirm: () => dispatch(muteAccount(account.get('id'))),
-    }));
+    dispatch(initMuteModal(account));
   },
 
   onMuteConversation (status) {
diff --git a/app/javascript/glitch/components/status/content.js b/app/javascript/glitch/components/status/content.js
index 06fe04ce0..06015619b 100644
--- a/app/javascript/glitch/components/status/content.js
+++ b/app/javascript/glitch/components/status/content.js
@@ -1,13 +1,11 @@
 //  Package imports  //
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import escapeTextContentForBrowser from 'escape-html';
 import PropTypes from 'prop-types';
 import { FormattedMessage } from 'react-intl';
 import classnames from 'classnames';
 
 //  Mastodon imports  //
-import emojify from '../../../mastodon/emoji';
 import { isRtl } from '../../../mastodon/rtl';
 import Permalink from '../../../mastodon/components/permalink';
 
@@ -32,7 +30,7 @@ export default class StatusContent extends React.PureComponent {
     const node  = this.node;
     const links = node.querySelectorAll('a');
 
-    for (var i = 0; i < links.length; ++i) {
+    for (let i = 0; i < links.length; ++i) {
       let link    = links[i];
       let mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
 
@@ -131,12 +129,8 @@ export default class StatusContent extends React.PureComponent {
       this.state.hidden
     );
 
-    const content = { __html: emojify(status.get('content')) };
-    const spoilerContent = {
-      __html: emojify(escapeTextContentForBrowser(
-        status.get('spoiler_text', '')
-      )),
-    };
+    const content = { __html: status.get('contentHtml') };
+    const spoilerContent = { __html: status.get('spoilerHtml') };
     const directionStyle = { direction: 'ltr' };
     const classNames = classnames('status__content', {
       'status__content--with-action': parseClick && !disabled,
@@ -188,7 +182,7 @@ export default class StatusContent extends React.PureComponent {
       }
 
       return (
-        <div className={classNames} ref={this.setRef}>
+        <div className={classNames}>
           <p
             style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}
             onMouseDown={this.handleMouseDown}
@@ -205,6 +199,7 @@ export default class StatusContent extends React.PureComponent {
 
           <div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
             <div
+              ref={this.setRef}
               style={directionStyle}
               onMouseDown={this.handleMouseDown}
               onMouseUp={this.handleMouseUp}
@@ -218,11 +213,11 @@ export default class StatusContent extends React.PureComponent {
     } else if (parseClick) {
       return (
         <div
-          ref={this.setRef}
           className={classNames}
           style={directionStyle}
         >
           <div
+            ref={this.setRef}
             onMouseDown={this.handleMouseDown}
             onMouseUp={this.handleMouseUp}
             dangerouslySetInnerHTML={content}
@@ -233,11 +228,10 @@ export default class StatusContent extends React.PureComponent {
     } else {
       return (
         <div
-          ref={this.setRef}
           className='status__content'
           style={directionStyle}
         >
-          <div dangerouslySetInnerHTML={content} />
+          <div ref={this.setRef} dangerouslySetInnerHTML={content} />
           {media}
         </div>
       );
diff --git a/app/javascript/glitch/components/status/gallery/item.js b/app/javascript/glitch/components/status/gallery/item.js
index d646825a3..ab4aab8dc 100644
--- a/app/javascript/glitch/components/status/gallery/item.js
+++ b/app/javascript/glitch/components/status/gallery/item.js
@@ -17,6 +17,24 @@ export default class StatusGalleryItem extends React.PureComponent {
     autoPlayGif: PropTypes.bool.isRequired,
   };
 
+  handleMouseEnter = (e) => {
+    if (this.hoverToPlay()) {
+      e.target.play();
+    }
+  }
+
+  handleMouseLeave = (e) => {
+    if (this.hoverToPlay()) {
+      e.target.pause();
+      e.target.currentTime = 0;
+    }
+  }
+
+  hoverToPlay () {
+    const { attachment, autoPlayGif } = this.props;
+    return !autoPlayGif && attachment.get('type') === 'gifv';
+  }
+
   handleClick = (e) => {
     const { index, onClick } = this.props;
 
@@ -112,6 +130,8 @@ export default class StatusGalleryItem extends React.PureComponent {
             role='application'
             src={attachment.get('url')}
             onClick={this.handleClick}
+            onMouseEnter={this.handleMouseEnter}
+            onMouseLeave={this.handleMouseLeave}
             autoPlay={autoPlay}
             loop
             muted
diff --git a/app/javascript/glitch/components/status/header.js b/app/javascript/glitch/components/status/header.js
index 5ce59fba4..f741950b1 100644
--- a/app/javascript/glitch/components/status/header.js
+++ b/app/javascript/glitch/components/status/header.js
@@ -9,41 +9,30 @@ component for better documentation and maintainance by
 
 */
 
-                            /* * * * */
+//  * * * * * * *  //
 
-/*
-
-Imports:
---------
+//  Imports
+//  -------
 
-*/
-
-//  Package imports  //
+//  Package imports.
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { defineMessages, injectIntl } from 'react-intl';
 
-//  Mastodon imports  //
+//  Mastodon imports.
 import Avatar from '../../../mastodon/components/avatar';
 import AvatarOverlay from '../../../mastodon/components/avatar_overlay';
 import DisplayName from '../../../mastodon/components/display_name';
 import IconButton from '../../../mastodon/components/icon_button';
 import VisibilityIcon from './visibility_icon';
 
-                            /* * * * */
-
-/*
-
-Inital setup:
--------------
+//  * * * * * * *  //
 
-The `messages` constant is used to define any messages that we need
-from inside props. In our case, these are the `collapse` and
-`uncollapse` messages used with our collapse/uncollapse buttons.
-
-*/
+//  Initial setup
+//  -------------
 
+//  Messages for use with internationalization stuff.
 const messages = defineMessages({
   collapse: { id: 'status.collapse', defaultMessage: 'Collapse' },
   uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' },
@@ -53,43 +42,10 @@ const messages = defineMessages({
   direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
 });
 
-                            /* * * * */
-
-/*
-
-The `<StatusHeader>` component:
--------------------------------
-
-The `<StatusHeader>` component wraps together the header information
-(avatar, display name) and upper buttons and icons (collapsing, media
-icons) into a single `<header>` element.
-
-###  Props
-
- -  __`account`, `friend` (`ImmutablePropTypes.map`) :__
-    These give the accounts associated with the status. `account` is
-    the author of the post; `friend` will have their avatar appear
-    in the overlay if provided.
-
- -  __`mediaIcon` (`PropTypes.string`) :__
-    If a mediaIcon should be placed in the header, this string
-    specifies it.
-
- -  __`collapsible`, `collapsed` (`PropTypes.bool`) :__
-    These props tell whether a post can be, and is, collapsed.
-
- -  __`parseClick` (`PropTypes.func`) :__
-    This function will be called when the user clicks inside the header
-    information.
-
- -  __`setExpansion` (`PropTypes.func`) :__
-    This function is used to set the expansion state of the post.
-
- -  __`intl` (`PropTypes.object`) :__
-    This is our internationalization object, provided by
-    `injectIntl()`.
+//  * * * * * * *  //
 
-*/
+//  The component
+//  -------------
 
 @injectIntl
 export default class StatusHeader extends React.PureComponent {
@@ -105,18 +61,7 @@ export default class StatusHeader extends React.PureComponent {
     intl: PropTypes.object.isRequired,
   };
 
-/*
-
-###  Implementation
-
-####  `handleCollapsedClick()`.
-
-`handleCollapsedClick()` is just a simple callback for our collapsing
-button. It calls `setExpansion` to set the collapsed state of the
-status.
-
-*/
-
+  //  Handles clicks on collapsed button
   handleCollapsedClick = (e) => {
     const { collapsed, setExpansion } = this.props;
     if (e.button === 0) {
@@ -125,29 +70,13 @@ status.
     }
   }
 
-/*
-
-####  `handleAccountClick()`.
-
-`handleAccountClick()` handles any clicks on the header info. It calls
-`parseClick()` with our `account` as the anticipatory `destination`.
-
-*/
-
+  //  Handles clicks on account name/image
   handleAccountClick = (e) => {
     const { status, parseClick } = this.props;
     parseClick(e, `/accounts/${+status.getIn(['account', 'id'])}`);
   }
 
-/*
-
-####  `render()`.
-
-`render()` actually puts our element on the screen. `<StatusHeader>`
-has a very straightforward rendering process.
-
-*/
-
+  //  Rendering.
   render () {
     const {
       status,
@@ -162,16 +91,28 @@ has a very straightforward rendering process.
 
     return (
       <header className='status__info'>
-        {
-
-/*
-
-We have to include the status icons before the header content because
-it is rendered as a float.
-
-*/
-
-        }
+        <a
+          href={account.get('url')}
+          target='_blank'
+          className='status__avatar'
+          onClick={this.handleAccountClick}
+        >
+          {
+            friend ? (
+              <AvatarOverlay account={account} friend={friend} />
+            ) : (
+              <Avatar account={account} size={48} />
+            )
+          }
+        </a>
+        <a
+          href={account.get('url')}
+          target='_blank'
+          className='status__display-name'
+          onClick={this.handleAccountClick}
+        >
+          <DisplayName account={account} />
+        </a>
         <div className='status__info__icons'>
           {mediaIcon ? (
             <i
@@ -197,39 +138,6 @@ it is rendered as a float.
             />
           ) : null}
         </div>
-        {
-
-/*
-
-This begins our header content. It is all wrapped inside of a link
-which gets handled by `handleAccountClick`. We use an `<AvatarOverlay>`
-if we have a `friend` and a normal `<Avatar>` if we don't.
-
-*/
-
-        }
-        <a
-          href={account.get('url')}
-          target='_blank'
-          className='status__display-name'
-          onClick={this.handleAccountClick}
-        >
-          <div className='status__avatar'>{
-            friend ? (
-              <AvatarOverlay
-                staticSrc={account.get('avatar_static')}
-                overlaySrc={friend.get('avatar_static')}
-              />
-            ) : (
-              <Avatar
-                src={account.get('avatar')}
-                staticSrc={account.get('avatar_static')}
-                size={48}
-              />
-            )
-          }</div>
-          <DisplayName account={account} />
-        </a>
 
       </header>
     );
diff --git a/app/javascript/glitch/components/status/index.js b/app/javascript/glitch/components/status/index.js
index 55e6f1876..9e758793c 100644
--- a/app/javascript/glitch/components/status/index.js
+++ b/app/javascript/glitch/components/status/index.js
@@ -155,20 +155,23 @@ export default class Status extends ImmutablePureComponent {
   };
 
   static propTypes = {
-    id                          : PropTypes.number,
+    id                          : PropTypes.string,
     status                      : ImmutablePropTypes.map,
     account                     : ImmutablePropTypes.map,
     settings                    : ImmutablePropTypes.map,
     notification                : ImmutablePropTypes.map,
-    me                          : PropTypes.number,
+    me                          : PropTypes.string,
     onFavourite                 : PropTypes.func,
     onReblog                    : PropTypes.func,
     onModalReblog               : PropTypes.func,
     onDelete                    : PropTypes.func,
+    onPin                       : PropTypes.func,
     onMention                   : PropTypes.func,
     onMute                      : PropTypes.func,
     onMuteConversation          : PropTypes.func,
     onBlock                     : PropTypes.func,
+    onEmbed                     : PropTypes.func,
+    onHeightChange              : PropTypes.func,
     onReport                    : PropTypes.func,
     onOpenMedia                 : PropTypes.func,
     onOpenVideo                 : PropTypes.func,
diff --git a/app/javascript/glitch/components/status/prepend.js b/app/javascript/glitch/components/status/prepend.js
index 6213e4c8d..8c0aed0f4 100644
--- a/app/javascript/glitch/components/status/prepend.js
+++ b/app/javascript/glitch/components/status/prepend.js
@@ -22,12 +22,8 @@ Imports:
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import escapeTextContentForBrowser from 'escape-html';
 import { FormattedMessage } from 'react-intl';
 
-//  Mastodon imports  //
-import emojify from '../../../mastodon/emoji';
-
                             /* * * * */
 
 /*
@@ -99,9 +95,7 @@ generate the message.
       >
         <b
           dangerouslySetInnerHTML={{
-            __html : emojify(escapeTextContentForBrowser(
-              account.get('display_name') || account.get('username')
-            )),
+            __html : account.get('display_name_html') || account.get('username'),
           }}
         />
       </a>