about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch/components')
-rw-r--r--app/javascript/flavours/glitch/components/media_gallery.js61
-rw-r--r--app/javascript/flavours/glitch/components/status.js8
-rw-r--r--app/javascript/flavours/glitch/components/status_action_bar.js25
-rw-r--r--app/javascript/flavours/glitch/components/status_content.js246
4 files changed, 316 insertions, 24 deletions
diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js
index 890a422d3..7521d4645 100644
--- a/app/javascript/flavours/glitch/components/media_gallery.js
+++ b/app/javascript/flavours/glitch/components/media_gallery.js
@@ -385,6 +385,66 @@ class MediaGallery extends React.PureComponent {
       );
     }
 
+    let parts = {};
+
+    media.map(
+      (attachment, i) => {
+        if (attachment.get('description')) {
+          if (attachment.get('description') in parts) {
+            parts[attachment.get('description')].push([i, attachment.get('url'), attachment.get('id')]);
+          } else {
+            parts[attachment.get('description')] = [[i, attachment.get('url'), attachment.get('id')]];
+          }
+        }
+      },
+    );
+
+    let descriptions = Object.entries(parts).map(
+      part => {
+        const [desc, idx] = part;
+        if (idx.length === 1) {
+          const url = idx[0][1];
+          return (
+            <p key={idx[0][2]}>
+              <strong>
+                <a href={url} title={url} target='_blank' rel='nofollow noopener'>
+                  <FormattedMessage id='status.media.description' defaultMessage='Attachment #{index}: ' values={{ index: 1+idx[0][0] }} />
+                </a>
+              </strong>
+              <span>{desc}</span>
+            </p>
+          );
+        } else if (idx.length !== 0) {
+          const indexes = (
+            <React.Fragment>
+              {
+                idx.map((i, c) => {
+                  const url = i[1];
+                  return (<span key={i[2]}>{c === 0 ? ' ' : ', '}<a href={url} title={url} target='_blank' rel='nofollow noopener'>#{1+i[0]}</a></span>);
+                })
+              }
+            </React.Fragment>
+          );
+          return (
+            <p key={idx[0][2]}>
+              <strong>
+                <FormattedMessage id='status.media.descriptions' defaultMessage='Attachments {list}: ' values={{ list: indexes }} />
+              </strong>
+              <span>{desc}</span>
+            </p>
+          );
+        } else {
+          return null;
+        }
+      },
+    );
+
+    let description_wrapper = visible && (
+      <div className='media-caption'>
+        {descriptions}
+      </div>
+    );
+
     return (
       <div className={computedClass} style={style} ref={this.handleRef}>
         <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}>
@@ -397,6 +457,7 @@ class MediaGallery extends React.PureComponent {
         </div>
 
         {children}
+        {description_wrapper}
       </div>
     );
   }
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index 1b7dce4c4..0de36ce2e 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -74,6 +74,8 @@ class Status extends ImmutablePureComponent {
     onReblog: PropTypes.func,
     onBookmark: PropTypes.func,
     onDelete: PropTypes.func,
+    onEdit: PropTypes.func,
+    onPublish: PropTypes.func,
     onDirect: PropTypes.func,
     onMention: PropTypes.func,
     onPin: PropTypes.func,
@@ -373,7 +375,7 @@ class Status extends ImmutablePureComponent {
   }
 
   handleExpandedToggle = () => {
-    if (this.props.status.get('spoiler_text')) {
+    if (this.props.status.get('spoiler_text') || this.props.status.get('reblogSpoilerHtml')) {
       this.setExpansion(!this.state.isExpanded);
     }
   };
@@ -689,6 +691,9 @@ class Status extends ImmutablePureComponent {
     //  Users can use those for theming, hiding avatars etc via UserStyle
     const selectorAttribs = {
       'data-status-by': `@${status.getIn(['account', 'acct'])}`,
+      'data-nest-level': status.get('nest_level'),
+      'data-nest-deep': status.get('nest_level') >= 15,
+      'data-local-only': !!status.get('local_only'),
     };
 
     if (prepend && account) {
@@ -710,6 +715,7 @@ class Status extends ImmutablePureComponent {
 
     const computedClass = classNames('status', `status-${status.get('visibility')}`, {
       collapsed: isCollapsed,
+      unpublished: status.get('published') === false,
       'has-background': isCollapsed && background,
       'status__wrapper-reply': !!status.get('in_reply_to_id'),
       unread,
diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js
index 2ccb02c62..b844590c0 100644
--- a/app/javascript/flavours/glitch/components/status_action_bar.js
+++ b/app/javascript/flavours/glitch/components/status_action_bar.js
@@ -13,6 +13,8 @@ import classNames from 'classnames';
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
   redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
+  edit: { id: 'status.edit', defaultMessage: 'Edit' },
+  publish: { id: 'status.publish', defaultMessage: 'Publish' },
   direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
   mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
   mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
@@ -53,6 +55,8 @@ class StatusActionBar extends ImmutablePureComponent {
     onFavourite: PropTypes.func,
     onReblog: PropTypes.func,
     onDelete: PropTypes.func,
+    onEdit: PropTypes.func,
+    onPublish: PropTypes.func,
     onDirect: PropTypes.func,
     onMention: PropTypes.func,
     onMute: PropTypes.func,
@@ -115,7 +119,7 @@ class StatusActionBar extends ImmutablePureComponent {
 
   _openInteractionDialog = type => {
     window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
-   }
+  }
 
   handleDeleteClick = () => {
     this.props.onDelete(this.props.status, this.context.router.history);
@@ -125,6 +129,14 @@ class StatusActionBar extends ImmutablePureComponent {
     this.props.onDelete(this.props.status, this.context.router.history, true);
   }
 
+  handleEditClick = () => {
+    this.props.onEdit(this.props.status, this.context.router.history);
+  }
+
+  handlePublishClick = () => {
+    this.props.onPublish(this.props.status);
+  }
+
   handlePinClick = () => {
     this.props.onPin(this.props.status);
   }
@@ -211,10 +223,8 @@ class StatusActionBar extends ImmutablePureComponent {
 
     menu.push(null);
 
-    if (status.getIn(['account', 'id']) === me || withDismiss) {
-      menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
-      menu.push(null);
-    }
+    menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
+    menu.push(null);
 
     if (status.getIn(['account', 'id']) === me) {
       if (publicStatus) {
@@ -223,6 +233,11 @@ class StatusActionBar extends ImmutablePureComponent {
 
       menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
       menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
+      menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
+
+      if (status.get('published') === false) {
+        menu.push({ text: intl.formatMessage(messages.publish), action: this.handlePublishClick });
+      }
     } else {
       menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
       menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js
index a39f747b8..a4546edfd 100644
--- a/app/javascript/flavours/glitch/components/status_content.js
+++ b/app/javascript/flavours/glitch/components/status_content.js
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 import { isRtl } from 'flavours/glitch/util/rtl';
 import { FormattedMessage } from 'react-intl';
 import Permalink from './permalink';
+import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
 import classnames from 'classnames';
 import Icon from 'flavours/glitch/components/icon';
 import { autoPlayGif } from 'flavours/glitch/util/initial_state';
@@ -13,7 +14,7 @@ const textMatchesTarget = (text, origin, host) => {
   return (text === origin || text === host
           || text.startsWith(origin + '/') || text.startsWith(host + '/')
           || 'www.' + text === host || ('www.' + text).startsWith(host + '/'));
-}
+};
 
 const isLinkMisleading = (link) => {
   let linkTextParts = [];
@@ -77,11 +78,13 @@ export default class StatusContent extends React.PureComponent {
     onUpdate: PropTypes.func,
     tagLinks: PropTypes.bool,
     rewriteMentions: PropTypes.string,
+    article: PropTypes.bool,
   };
 
   static defaultProps = {
     tagLinks: true,
     rewriteMentions: 'no',
+    article: false,
   };
 
   state = {
@@ -231,7 +234,7 @@ export default class StatusContent extends React.PureComponent {
 
     let element = e.target;
     while (element) {
-      if (['button', 'video', 'a', 'label', 'canvas'].includes(element.localName)) {
+      if (['button', 'video', 'a', 'label', 'canvas', 'details', 'summary'].includes(element.localName)) {
         return;
       }
       element = element.parentNode;
@@ -271,23 +274,213 @@ export default class StatusContent extends React.PureComponent {
       disabled,
       tagLinks,
       rewriteMentions,
+      article,
     } = this.props;
 
     const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
 
-    const content = { __html: status.get('contentHtml') };
-    const spoilerContent = { __html: status.get('spoilerHtml') };
+    const edited = (status.get('edited') === 0) ? null : (
+      <div className='status__notice status__edit-notice'>
+        <Icon id='pencil-square-o' />
+        <FormattedMessage
+          id='status.edited'
+          defaultMessage='{count, plural, one {# edit} other {# edits}} · last update: {updated_at}'
+          key={`edit-${status.get('id')}`}
+          values={{
+            count: status.get('edited'),
+            updated_at: <RelativeTimestamp timestamp={status.get('updated_at')} />,
+          }}
+        />
+      </div>
+    );
+
+    const unpublished = (status.get('published') === false) && (
+      <div className='status__notice status__unpublished-notice'>
+        <Icon id='chain-broken' />
+        <FormattedMessage
+          id='status.unpublished'
+          defaultMessage='Unpublished'
+          key={`unpublished-${status.get('id')}`}
+        />
+      </div>
+    );
+
+    const local_only = (status.get('local_only') === true) && (
+      <div className='status__notice status__localonly-notice'>
+        <Icon id='home' />
+        <FormattedMessage
+          id='advanced_options.local-only.short'
+          defaultMessage='Local-only'
+          key={`localonly-${status.get('id')}`}
+        />
+      </div>
+    );
+
+    const quiet = (status.get('notify') === false) && (
+      <div className='status__notice status__quiet-notice'>
+        <Icon id='bell-slash' />
+        <FormattedMessage
+          id='status.quiet'
+          defaultMessage='Quiet local publish'
+          key={`quiet-${status.get('id')}`}
+        />
+      </div>
+    );
+
+    const article_content = status.get('article') && (
+      <div className='status__notice status__article-notice'>
+        <Icon id='file-text-o' />
+        <Permalink
+          href={status.get('url')}
+          to={`/statuses/${status.get('id')}`}
+        >
+          <FormattedMessage
+            id='status.article'
+            defaultMessage='Article'
+            key={`article-${status.get('id')}`}
+          />
+        </Permalink>
+      </div>
+    );
+
+    const publish_at = status.get('publish_at') && (
+      <div className='status__notice status__publish-notice'>
+        <Icon id='bullhorn' />
+        <FormattedMessage
+          id='status.publish_at'
+          defaultMessage='Auto-publish: {publish_at}'
+          key={`publish-${status.get('id')}`}
+          values={{
+            publish_at: <RelativeTimestamp timestamp={status.get('publish_at')} futureDate />,
+          }}
+        />
+      </div>
+    );
+
+    const expires_at = !unpublished && status.get('expires_at') && (
+      <div className='status__notice status__expires-notice'>
+        <Icon id='clock-o' />
+        <FormattedMessage
+          id='status.expires_at'
+          defaultMessage='Self-destruct: {expires_at}'
+          key={`expires-${status.get('id')}`}
+          values={{
+            expires_at: <RelativeTimestamp timestamp={status.get('expires_at')} futureDate />,
+          }}
+        />
+      </div>
+    );
+
+    const status_notice_wrapper = (
+      <div className='status__notice-wrapper'>
+        {unpublished}
+        {publish_at}
+        {expires_at}
+        {quiet}
+        {edited}
+        {local_only}
+        {article_content}
+      </div>
+    );
+
+    const permissions_present = status.get('domain_permissions') && status.get('domain_permissions').size > 0;
+
+    const status_permission_items = permissions_present && status.get('domain_permissions').map((permission) => (
+      <li className='permission-status'>
+        <Icon id='eye-slash' />
+        <FormattedMessage
+          id='status.permissions.visibility.status'
+          defaultMessage='{visibility} 🡲 {domain}'
+          key={`permissions-visibility-${status.get('id')}`}
+          values={{
+            domain: <span>{permission.get('domain')}</span>,
+            visibility: <span>{permission.get('visibility')}</span>,
+          }}
+        />
+      </li>
+    ));
+
+    const permissions = status_permission_items && (
+      <details className='status__permissions' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
+        <summary>
+          <Icon id='unlock-alt' />
+          <FormattedMessage
+            id='status.permissions.title'
+            defaultMessage='Show extended permissions...'
+            key={`permissions-${status.get('id')}`}
+          />
+        </summary>
+        <ul>
+          {status_permission_items}
+        </ul>
+      </details>
+    );
+
+    const tag_items = (status.get('tags') && status.get('tags').size > 0) && status.get('tags').map(hashtag =>
+      (
+        <li>
+          <Icon id='tag' />
+          <Permalink
+            href={hashtag.get('url')}
+            to={`/timelines/tag/${hashtag.get('name')}`}
+          >
+            <span>{hashtag.get('name')}</span>
+          </Permalink>
+        </li>
+      ));
+
+    const tags = tag_items && (
+      <details className='status__tags' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
+        <summary>
+          <Icon id='tag' />
+          <FormattedMessage
+            id='status.tags'
+            defaultMessage='Show all tags...'
+            key={`tags-${status.get('id')}`}
+          />
+        </summary>
+        <ul>
+          {tag_items}
+        </ul>
+      </details>
+    );
+
+    const footers = (
+      <div className='status__footers'>
+        {permissions}
+        {tags}
+      </div>
+    );
+
+    const reblog_spoiler_html = status.get('reblogSpoilerPresent') && { __html: status.get('reblogSpoilerHtml') };
+    const reblog_spoiler = reblog_spoiler_html && (
+      <div className='reblog-spoiler spoiler'>
+        <Icon id='retweet' />
+        <span dangerouslySetInnerHTML={reblog_spoiler_html} />
+      </div>
+    );
+
+    const spoiler_html = status.get('spoiler_text').length > 0 && { __html: status.get('spoilerHtml') };
+    const spoiler = spoiler_html && (
+      <div className='spoiler'>
+        <Icon id='info-circle' />
+        <span dangerouslySetInnerHTML={spoiler_html} />
+      </div>
+    );
+
+    const spoiler_present = status.get('spoiler_text').length > 0 || status.get('reblogSpoilerPresent');
+    const content = { __html: article ? status.get('articleHtml') : status.get('contentHtml') };
     const directionStyle = { direction: 'ltr' };
     const classNames = classnames('status__content', {
       'status__content--with-action': parseClick && !disabled,
-      'status__content--with-spoiler': status.get('spoiler_text').length > 0,
+      'status__content--with-spoiler': spoiler_present,
     });
 
     if (isRtl(status.get('search_index'))) {
       directionStyle.direction = 'rtl';
     }
 
-    if (status.get('spoiler_text').length > 0) {
+    if (spoiler_present) {
       let mentionsPlaceholder = '';
 
       const mentionLinks = status.get('mentions').map(item => (
@@ -302,11 +495,19 @@ export default class StatusContent extends React.PureComponent {
       )).reduce((aggregate, item) => [...aggregate, item, ' '], []);
 
       const toggleText = hidden ? [
-        <FormattedMessage
-          id='status.show_more'
-          defaultMessage='Show more'
-          key='0'
-        />,
+        article ? (
+          <FormattedMessage
+            id='status.show_article'
+            defaultMessage='Show article'
+            key='0'
+          />
+        ) : (
+          <FormattedMessage
+            id='status.show_more'
+            defaultMessage='Show more'
+            key='0'
+          />
+        ),
         mediaIcon ? (
           <Icon
             fixedWidth
@@ -330,15 +531,18 @@ export default class StatusContent extends React.PureComponent {
 
       return (
         <div className={classNames} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} ref={this.setRef}>
-          <p
+          {status_notice_wrapper}
+          <div
             style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}
           >
-            <span dangerouslySetInnerHTML={spoilerContent} />
-            {' '}
-            <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>
-              {toggleText}
-            </button>
-          </p>
+            {reblog_spoiler}
+            {spoiler}
+            <div class='spoiler-actions'>
+              <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>
+                {toggleText}
+              </button>
+            </div>
+          </div>
 
           {mentionsPlaceholder}
 
@@ -354,6 +558,8 @@ export default class StatusContent extends React.PureComponent {
             {media}
           </div>
 
+          {footers}
+
         </div>
       );
     } else if (parseClick) {
@@ -366,6 +572,7 @@ export default class StatusContent extends React.PureComponent {
           tabIndex='0'
           ref={this.setRef}
         >
+          {status_notice_wrapper}
           <div
             ref={this.setContentsRef}
             key={`contents-${tagLinks}-${rewriteMentions}`}
@@ -374,6 +581,7 @@ export default class StatusContent extends React.PureComponent {
             tabIndex='0'
           />
           {media}
+          {footers}
         </div>
       );
     } else {
@@ -384,8 +592,10 @@ export default class StatusContent extends React.PureComponent {
           tabIndex='0'
           ref={this.setRef}
         >
+          {status_notice_wrapper}
           <div ref={this.setContentsRef} key={`contents-${tagLinks}`} className='status__content__text' dangerouslySetInnerHTML={content} tabIndex='0' />
           {media}
+          {footers}
         </div>
       );
     }