about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/components
diff options
context:
space:
mode:
authorReverite <github@reverite.sh>2019-02-24 22:18:10 -0800
committerReverite <github@reverite.sh>2019-02-24 22:18:10 -0800
commit54e480ca0939ba737f5abdf4ee861cd63c025865 (patch)
tree760cdd75fd0922266b60e784c06db99902ef5692 /app/javascript/flavours/glitch/components
parentff9a09a9a7f73b558c53f334573b94198eb8d08a (diff)
parentd82de360c13894746d3974d11c9505c8937ebdee (diff)
Merge branch 'glitch' into production
Diffstat (limited to 'app/javascript/flavours/glitch/components')
-rw-r--r--app/javascript/flavours/glitch/components/account.js2
-rw-r--r--app/javascript/flavours/glitch/components/domain.js2
-rw-r--r--app/javascript/flavours/glitch/components/intersection_observer_article.js29
-rw-r--r--app/javascript/flavours/glitch/components/media_gallery.js10
-rw-r--r--app/javascript/flavours/glitch/components/scrollable_list.js14
-rw-r--r--app/javascript/flavours/glitch/components/status.js37
-rw-r--r--app/javascript/flavours/glitch/components/status_action_bar.js51
-rw-r--r--app/javascript/flavours/glitch/components/status_content.js2
-rw-r--r--app/javascript/flavours/glitch/components/status_header.js2
-rw-r--r--app/javascript/flavours/glitch/components/status_prepend.js2
-rw-r--r--app/javascript/flavours/glitch/components/status_visibility_icon.js2
11 files changed, 110 insertions, 43 deletions
diff --git a/app/javascript/flavours/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.js
index 072c601e0..4fcafc509 100644
--- a/app/javascript/flavours/glitch/components/account.js
+++ b/app/javascript/flavours/glitch/components/account.js
@@ -85,7 +85,7 @@ export default class Account extends ImmutablePureComponent {
       if (requested) {
         buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
       } else if (blocking) {
-        buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
+        buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
       } else if (muting) {
         let hidingNotificationsButton;
         if (account.getIn(['relationship', 'muting_notifications'])) {
diff --git a/app/javascript/flavours/glitch/components/domain.js b/app/javascript/flavours/glitch/components/domain.js
index f657cb8d2..74174f83d 100644
--- a/app/javascript/flavours/glitch/components/domain.js
+++ b/app/javascript/flavours/glitch/components/domain.js
@@ -32,7 +32,7 @@ export default class Account extends ImmutablePureComponent {
           </span>
 
           <div className='domain__buttons'>
-            <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
+            <IconButton active icon='unlock' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
           </div>
         </div>
       </div>
diff --git a/app/javascript/flavours/glitch/components/intersection_observer_article.js b/app/javascript/flavours/glitch/components/intersection_observer_article.js
index 6eeca5598..900c98638 100644
--- a/app/javascript/flavours/glitch/components/intersection_observer_article.js
+++ b/app/javascript/flavours/glitch/components/intersection_observer_article.js
@@ -63,7 +63,7 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
   }
 
   updateStateAfterIntersection = (prevState) => {
-    if (prevState.isIntersecting && !this.entry.isIntersecting) {
+    if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
       scheduleIdleTask(this.hideIfNotIntersecting);
     }
     return {
@@ -103,24 +103,23 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
     const { children, id, index, listLength, cachedHeight } = this.props;
     const { isIntersecting, isHidden } = this.state;
 
+    const style = {};
+
     if (!isIntersecting && (isHidden || cachedHeight)) {
-      return (
-        <article
-          ref={this.handleRef}
-          aria-posinset={index + 1}
-          aria-setsize={listLength}
-          style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
-          data-id={id}
-          tabIndex='0'
-        >
-          {children && React.cloneElement(children, { hidden: true })}
-        </article>
-      );
+      style.height = `${this.height || cachedHeight || 150}px`;
+      style.opacity = 0;
+      style.overflow = 'hidden';
     }
 
     return (
-      <article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex='0'>
-        {children && React.cloneElement(children, { hidden: false })}
+      <article
+        ref={this.handleRef}
+        aria-posinset={index + 1}
+        aria-setsize={listLength}
+        data-id={id}
+        tabIndex='0'
+        style={style}>
+          {children && React.cloneElement(children, { hidden: !isIntersecting && (isHidden || cachedHeight) })}
       </article>
     );
   }
diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js
index d0226bbbb..1fa25ee4e 100644
--- a/app/javascript/flavours/glitch/components/media_gallery.js
+++ b/app/javascript/flavours/glitch/components/media_gallery.js
@@ -224,6 +224,8 @@ export default class MediaGallery extends React.PureComponent {
     size: PropTypes.object,
     onOpenMedia: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
+    defaultWidth: PropTypes.number,
+    cacheWidth: PropTypes.func,
   };
 
   static defaultProps = {
@@ -232,6 +234,7 @@ export default class MediaGallery extends React.PureComponent {
 
   state = {
     visible: this.props.revealed === undefined ? (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all') : this.props.revealed,
+    width: this.props.defaultWidth,
   };
 
   componentWillReceiveProps (nextProps) {
@@ -259,6 +262,7 @@ export default class MediaGallery extends React.PureComponent {
   handleRef = (node) => {
     this.node = node;
     if (node && node.offsetWidth && node.offsetWidth != this.state.width) {
+      if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
       this.setState({
         width: node.offsetWidth,
       });
@@ -271,10 +275,12 @@ export default class MediaGallery extends React.PureComponent {
   }
 
   render () {
-    const { media, intl, sensitive, letterbox, fullwidth } = this.props;
-    const { width, visible } = this.state;
+    const { media, intl, sensitive, letterbox, fullwidth, defaultWidth } = this.props;
+    const { visible } = this.state;
     const size = media.take(4).size;
 
+    const width = this.state.width || defaultWidth;
+
     let children;
 
     const style = {};
diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js
index 7cd0774a9..462185bbc 100644
--- a/app/javascript/flavours/glitch/components/scrollable_list.js
+++ b/app/javascript/flavours/glitch/components/scrollable_list.js
@@ -40,6 +40,7 @@ export default class ScrollableList extends PureComponent {
 
   state = {
     fullscreen: null,
+    cachedMediaWidth: 300,
   };
 
   intersectionObserverWrapper = new IntersectionObserverWrapper();
@@ -128,7 +129,7 @@ export default class ScrollableList extends PureComponent {
   }
 
   getScrollPosition = () => {
-    if (this.node && this.node.scrollTop > 0) {
+    if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) {
       return {height: this.node.scrollHeight, top: this.node.scrollTop};
     } else {
       return null;
@@ -141,6 +142,10 @@ export default class ScrollableList extends PureComponent {
     this.setScrollTop(newScrollTop);
   }
 
+  cacheMediaWidth = (width) => {
+    if (width && this.state.cachedMediaWidth != width) this.setState({ cachedMediaWidth: width });
+  }
+
   getSnapshotBeforeUpdate (prevProps, prevState) {
     const someItemInserted = React.Children.count(prevProps.children) > 0 &&
       React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
@@ -252,7 +257,12 @@ export default class ScrollableList extends PureComponent {
                 intersectionObserverWrapper={this.intersectionObserverWrapper}
                 saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
               >
-                {React.cloneElement(child, {getScrollPosition: this.getScrollPosition, updateScrollBottom: this.updateScrollBottom})}
+                {React.cloneElement(child, {
+                  getScrollPosition: this.getScrollPosition,
+                  updateScrollBottom: this.updateScrollBottom,
+                  cachedMediaWidth: this.state.cachedMediaWidth,
+                  cacheMediaWidth: this.cacheMediaWidth,
+                })}
               </IntersectionObserverArticleContainer>
             ))}
 
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index 9ff53485e..349f9c6cc 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -72,6 +72,8 @@ export default class Status extends ImmutablePureComponent {
     updateScrollBottom: PropTypes.func,
     expanded: PropTypes.bool,
     intl: PropTypes.object.isRequired,
+    cacheMediaWidth: PropTypes.func,
+    cachedMediaWidth: PropTypes.number,
   };
 
   state = {
@@ -214,6 +216,8 @@ export default class Status extends ImmutablePureComponent {
       // Hack to fix timeline jumps on second rendering when auto-collapsing
       this.setState({ autoCollapsed: true });
     }
+
+    this.didShowCard  = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card') && this.props.settings.get('inline_preview_cards');
   }
 
   getSnapshotBeforeUpdate (prevProps, prevState) {
@@ -226,8 +230,10 @@ export default class Status extends ImmutablePureComponent {
 
   //  Hack to fix timeline jumps on second rendering when auto-collapsing
   componentDidUpdate (prevProps, prevState, snapshot) {
-    if (this.state.autoCollapsed) {
-      this.setState({ autoCollapsed: false });
+    const doShowCard  = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card') && this.props.settings.get('inline_preview_cards');
+    if (this.state.autoCollapsed || (doShowCard && !this.didShowCard)) {
+      if (doShowCard) this.didShowCard = true;
+      if (this.state.autoCollapsed) this.setState({ autoCollapsed: false });
       if (snapshot !== null && this.props.updateScrollBottom) {
         if (this.node.offsetTop < snapshot.top) {
           this.props.updateScrollBottom(snapshot.height - snapshot.top);
@@ -236,6 +242,15 @@ export default class Status extends ImmutablePureComponent {
     }
   }
 
+  componentWillUnmount() {
+    if (this.node && this.props.getScrollPosition) {
+      const position = this.props.getScrollPosition();
+      if (position !== null && this.node.offsetTop < position.top) {
+         requestAnimationFrame(() => { this.props.updateScrollBottom(position.height - position.top); });
+      }
+    }
+  }
+
   //  `setCollapsed()` sets the value of `isCollapsed` in our state, that is,
   //  whether the toot is collapsed or not.
 
@@ -384,15 +399,7 @@ export default class Status extends ImmutablePureComponent {
 
     if (hidden) {
       return (
-        <div
-          ref={this.handleRef}
-          data-id={status.get('id')}
-          style={{
-            height: `${this.height}px`,
-            opacity: 0,
-            overflow: 'hidden',
-          }}
-        >
+        <div ref={this.handleRef}>
           {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
           {' '}
           {status.get('content')}
@@ -408,7 +415,7 @@ export default class Status extends ImmutablePureComponent {
 
       return (
         <HotKeys handlers={minHandlers}>
-          <div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0'>
+          <div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
             <FormattedMessage id='status.filtered' defaultMessage='Filtered' />
           </div>
         </HotKeys>
@@ -453,6 +460,8 @@ export default class Status extends ImmutablePureComponent {
               fullwidth={settings.getIn(['media', 'fullwidth'])}
               preventPlayback={isCollapsed || !isExpanded}
               onOpenVideo={this.handleOpenVideo}
+              width={this.props.cachedMediaWidth}
+              cacheWidth={this.props.cacheMediaWidth}
             />)}
           </Bundle>
         );
@@ -468,6 +477,8 @@ export default class Status extends ImmutablePureComponent {
                 fullwidth={settings.getIn(['media', 'fullwidth'])}
                 hidden={isCollapsed || !isExpanded}
                 onOpenMedia={this.props.onOpenMedia}
+                cacheWidth={this.props.cacheMediaWidth}
+                defaultWidth={this.props.cachedMediaWidth}
               />
             )}
           </Bundle>
@@ -484,6 +495,8 @@ export default class Status extends ImmutablePureComponent {
           onOpenMedia={this.props.onOpenMedia}
           card={status.get('card')}
           compact
+          cacheWidth={this.props.cacheMediaWidth}
+          defaultWidth={this.props.cachedMediaWidth}
         />
       );
       mediaIcon = 'link';
diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js
index 16abcab4e..1d3130604 100644
--- a/app/javascript/flavours/glitch/components/status_action_bar.js
+++ b/app/javascript/flavours/glitch/components/status_action_bar.js
@@ -34,6 +34,7 @@ const messages = defineMessages({
   embed: { id: 'status.embed', defaultMessage: 'Embed' },
   admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
   admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
+  copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
 });
 
 const obfuscatedCount = count => {
@@ -82,7 +83,11 @@ export default class StatusActionBar extends ImmutablePureComponent {
   ]
 
   handleReplyClick = () => {
-    this.props.onReply(this.props.status, this.context.router.history);
+    if (me) {
+      this.props.onReply(this.props.status, this.context.router.history);
+    } else {
+      this._openInteractionDialog('reply');
+    }
   }
 
   handleShareClick = () => {
@@ -93,17 +98,29 @@ export default class StatusActionBar extends ImmutablePureComponent {
   }
 
   handleFavouriteClick = (e) => {
-    this.props.onFavourite(this.props.status, e);
+    if (me) {
+      this.props.onFavourite(this.props.status, e);
+    } else {
+      this._openInteractionDialog('favourite');
+    }
   }
 
   handleBookmarkClick = (e) => {
     this.props.onBookmark(this.props.status, e);
   }
 
-  handleReblogClick = (e) => {
-    this.props.onReblog(this.props.status, e);
+  handleReblogClick = e => {
+    if (me) {
+      this.props.onReblog(this.props.status, e);
+    } else {
+      this._openInteractionDialog('reblog');
+    }
   }
 
+  _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);
   }
@@ -148,13 +165,32 @@ export default class StatusActionBar extends ImmutablePureComponent {
     this.props.onMuteConversation(this.props.status);
   }
 
+  handleCopy = () => {
+    const url      = this.props.status.get('url');
+    const textarea = document.createElement('textarea');
+
+    textarea.textContent    = url;
+    textarea.style.position = 'fixed';
+
+    document.body.appendChild(textarea);
+
+    try {
+      textarea.select();
+      document.execCommand('copy');
+    } catch (e) {
+
+    } finally {
+      document.body.removeChild(textarea);
+    }
+  }
+
   render () {
     const { status, intl, withDismiss, showReplyCount } = this.props;
 
     const mutingConversation = status.get('muted');
     const anonymousAccess    = !me;
     const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility'));
-    const reblogDisabled     = anonymousAccess || (status.get('visibility') === 'direct' || (status.get('visibility') === 'private' && me !== status.getIn(['account', 'id'])));
+    const reblogDisabled     = status.get('visibility') === 'direct' || (status.get('visibility') === 'private' && me !== status.getIn(['account', 'id']));
     const reblogMessage      = status.get('visibility') === 'private' ? messages.reblog_private : messages.reblog;
 
     let menu = [];
@@ -165,6 +201,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
     menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
 
     if (publicStatus) {
+      menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
       menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
     }
 
@@ -189,6 +226,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
       menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
       menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
       menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
+
       if (isStaff && (accountAdminLink || statusAdminLink)) {
         menu.push(null);
         if (accountAdminLink !== undefined) {
@@ -221,7 +259,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
     let replyButton = (
       <IconButton
         className='status__action-bar-button'
-        disabled={anonymousAccess}
         title={replyTitle}
         icon={replyIcon}
         onClick={this.handleReplyClick}
@@ -240,7 +277,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
       <div className='status__action-bar'>
         {replyButton}
         <IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(reblogMessage)} 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} />
+        <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
         {shareButton}
         <IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
 
diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js
index 6542df65b..c60d63f9a 100644
--- a/app/javascript/flavours/glitch/components/status_content.js
+++ b/app/javascript/flavours/glitch/components/status_content.js
@@ -17,6 +17,7 @@ export default class StatusContent extends React.PureComponent {
     mediaIcon: PropTypes.string,
     parseClick: PropTypes.func,
     disabled: PropTypes.bool,
+    onUpdate: PropTypes.func,
   };
 
   state = {
@@ -62,6 +63,7 @@ export default class StatusContent extends React.PureComponent {
 
   componentDidUpdate () {
     this._updateStatusLinks();
+    if (this.props.onUpdate) this.props.onUpdate();
   }
 
   onLinkClick = (e) => {
diff --git a/app/javascript/flavours/glitch/components/status_header.js b/app/javascript/flavours/glitch/components/status_header.js
index 65458e3f0..f9321904c 100644
--- a/app/javascript/flavours/glitch/components/status_header.js
+++ b/app/javascript/flavours/glitch/components/status_header.js
@@ -19,7 +19,7 @@ export default class StatusHeader extends React.PureComponent {
   //  Handles clicks on account name/image
   handleAccountClick = (e) => {
     const { status, parseClick } = this.props;
-    parseClick(e, `/accounts/${+status.getIn(['account', 'id'])}`);
+    parseClick(e, `/accounts/${status.getIn(['account', 'id'])}`);
   }
 
   //  Rendering.
diff --git a/app/javascript/flavours/glitch/components/status_prepend.js b/app/javascript/flavours/glitch/components/status_prepend.js
index f4ef83135..4e329f546 100644
--- a/app/javascript/flavours/glitch/components/status_prepend.js
+++ b/app/javascript/flavours/glitch/components/status_prepend.js
@@ -15,7 +15,7 @@ export default class StatusPrepend extends React.PureComponent {
 
   handleClick = (e) => {
     const { account, parseClick } = this.props;
-    parseClick(e, `/accounts/${+account.get('id')}`);
+    parseClick(e, `/accounts/${account.get('id')}`);
   }
 
   Message = () => {
diff --git a/app/javascript/flavours/glitch/components/status_visibility_icon.js b/app/javascript/flavours/glitch/components/status_visibility_icon.js
index 017b69cbb..5e7b8ed00 100644
--- a/app/javascript/flavours/glitch/components/status_visibility_icon.js
+++ b/app/javascript/flavours/glitch/components/status_visibility_icon.js
@@ -25,7 +25,7 @@ export default class VisibilityIcon extends ImmutablePureComponent {
 
     const visibilityClass = {
       public: 'globe',
-      unlisted: 'unlock-alt',
+      unlisted: 'unlock',
       private: 'lock',
       direct: 'envelope',
     }[visibility];