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/avatar_composite.js104
-rw-r--r--app/javascript/flavours/glitch/components/display_name.js44
-rw-r--r--app/javascript/flavours/glitch/components/status.js23
-rw-r--r--app/javascript/flavours/glitch/components/status_action_bar.js20
-rw-r--r--app/javascript/flavours/glitch/components/status_header.js82
-rw-r--r--app/javascript/flavours/glitch/components/status_icons.js6
6 files changed, 229 insertions, 50 deletions
diff --git a/app/javascript/flavours/glitch/components/avatar_composite.js b/app/javascript/flavours/glitch/components/avatar_composite.js
new file mode 100644
index 000000000..c52df043a
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/avatar_composite.js
@@ -0,0 +1,104 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { autoPlayGif } from 'flavours/glitch/util/initial_state';
+
+export default class AvatarComposite extends React.PureComponent {
+
+  static propTypes = {
+    accounts: ImmutablePropTypes.list.isRequired,
+    animate: PropTypes.bool,
+    size: PropTypes.number.isRequired,
+  };
+
+  static defaultProps = {
+    animate: autoPlayGif,
+  };
+
+  renderItem (account, size, index) {
+    const { animate } = this.props;
+
+    let width  = 50;
+    let height = 100;
+    let top    = 'auto';
+    let left   = 'auto';
+    let bottom = 'auto';
+    let right  = 'auto';
+
+    if (size === 1) {
+      width = 100;
+    }
+
+    if (size === 4 || (size === 3 && index > 0)) {
+      height = 50;
+    }
+
+    if (size === 2) {
+      if (index === 0) {
+        right = '2px';
+      } else {
+        left = '2px';
+      }
+    } else if (size === 3) {
+      if (index === 0) {
+        right = '2px';
+      } else if (index > 0) {
+        left = '2px';
+      }
+
+      if (index === 1) {
+        bottom = '2px';
+      } else if (index > 1) {
+        top = '2px';
+      }
+    } else if (size === 4) {
+      if (index === 0 || index === 2) {
+        right = '2px';
+      }
+
+      if (index === 1 || index === 3) {
+        left = '2px';
+      }
+
+      if (index < 2) {
+        bottom = '2px';
+      } else {
+        top = '2px';
+      }
+    }
+
+    const style = {
+      left: left,
+      top: top,
+      right: right,
+      bottom: bottom,
+      width: `${width}%`,
+      height: `${height}%`,
+      backgroundSize: 'cover',
+      backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
+    };
+
+    return (
+      <a
+        href={account.get('url')}
+        target='_blank'
+        onClick={(e) => this.props.onAccountClick(account.get('id'), e)}
+        title={`@${account.get('acct')}`}
+        key={account.get('id')}
+      >
+        <div style={style} data-avatar-of={`@${account.get('acct')}`} />
+      </a>
+    );
+  }
+
+  render() {
+    const { accounts, size } = this.props;
+
+    return (
+      <div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
+        {accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/components/display_name.js b/app/javascript/flavours/glitch/components/display_name.js
index a26cff049..7f6ef5a5d 100644
--- a/app/javascript/flavours/glitch/components/display_name.js
+++ b/app/javascript/flavours/glitch/components/display_name.js
@@ -10,24 +10,56 @@ export default function DisplayName ({
   className,
   inline,
   localDomain,
+  others,
+  onAccountClick,
 }) {
   const computedClass = classNames('display-name', { inline }, className);
 
   if (!account) return null;
 
+  let displayName, suffix;
+
   let acct = account.get('acct');
+
   if (acct.indexOf('@') === -1 && localDomain) {
     acct = `${acct}@${localDomain}`;
   }
 
-  //  The result.
-  return account ? (
+  if (others && others.size > 0) {
+    displayName = others.take(2).map(a => (
+      <a
+        href={a.get('url')}
+        target='_blank'
+        onClick={(e) => onAccountClick(a.get('id'), e)}
+        title={`@${a.get('acct')}`}
+      >
+        <bdi key={a.get('id')}>
+          <strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} />
+        </bdi>
+      </a>
+    )).reduce((prev, cur) => [prev, ', ', cur]);
+
+    if (others.size - 2 > 0) {
+     displayName.push(` +${others.size - 2}`);
+    }
+
+    suffix = (
+      <a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('id'), e)}>
+        <span className='display-name__account'>@{acct}</span>
+      </a>
+    );
+  } else {
+    displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
+    suffix      = <span className='display-name__account'>@{acct}</span>;
+  }
+
+  return (
     <span className={computedClass}>
-      <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>
+      {displayName}
       {inline ? ' ' : null}
-      <span className='display-name__account'>@{acct}</span>
+      {suffix}
     </span>
-  ) : null;
+  );
 }
 
 //  Props.
@@ -36,4 +68,6 @@ DisplayName.propTypes = {
   className: PropTypes.string,
   inline: PropTypes.bool,
   localDomain: PropTypes.string,
+  others: ImmutablePropTypes.list,
+  handleClick: PropTypes.func,
 };
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index 7014cab17..4b9364ae5 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -66,6 +66,7 @@ export default class Status extends ImmutablePureComponent {
     containerId: PropTypes.string,
     id: PropTypes.string,
     status: ImmutablePropTypes.map,
+    otherAccounts: ImmutablePropTypes.list,
     account: ImmutablePropTypes.map,
     onReply: PropTypes.func,
     onFavourite: PropTypes.func,
@@ -83,6 +84,7 @@ export default class Status extends ImmutablePureComponent {
     muted: PropTypes.bool,
     collapse: PropTypes.bool,
     hidden: PropTypes.bool,
+    unread: PropTypes.bool,
     prepend: PropTypes.string,
     withDismiss: PropTypes.bool,
     onMoveUp: PropTypes.func,
@@ -93,6 +95,7 @@ export default class Status extends ImmutablePureComponent {
     intl: PropTypes.object.isRequired,
     cacheMediaWidth: PropTypes.func,
     cachedMediaWidth: PropTypes.number,
+    onClick: PropTypes.func,
   };
 
   state = {
@@ -321,17 +324,21 @@ export default class Status extends ImmutablePureComponent {
     const { status } = this.props;
     const { isCollapsed } = this.state;
     if (!router) return;
-    if (destination === undefined) {
-      destination = `/statuses/${
-        status.getIn(['reblog', 'id'], status.get('id'))
-      }`;
-    }
+
     if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
       if (isCollapsed) this.setCollapsed(false);
       else if (e.shiftKey) {
         this.setCollapsed(true);
         document.getSelection().removeAllRanges();
+      } else if (this.props.onClick) {
+        this.props.onClick();
+        return;
       } else {
+        if (destination === undefined) {
+          destination = `/statuses/${
+            status.getIn(['reblog', 'id'], status.get('id'))
+          }`;
+        }
         let state = {...router.history.location.state};
         state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
         router.history.push(destination, state);
@@ -441,6 +448,7 @@ export default class Status extends ImmutablePureComponent {
       intl,
       status,
       account,
+      otherAccounts,
       settings,
       collapsed,
       muted,
@@ -450,6 +458,7 @@ export default class Status extends ImmutablePureComponent {
       onOpenMedia,
       notification,
       hidden,
+      unread,
       featured,
       ...other
     } = this.props;
@@ -617,6 +626,7 @@ export default class Status extends ImmutablePureComponent {
       collapsed: isCollapsed,
       'has-background': isCollapsed && background,
       'status__wrapper-reply': !!status.get('in_reply_to_id'),
+      read: unread === false,
       muted,
     }, 'focusable');
 
@@ -647,6 +657,7 @@ export default class Status extends ImmutablePureComponent {
                   friend={account}
                   collapsed={isCollapsed}
                   parseClick={parseClick}
+                  otherAccounts={otherAccounts}
                 />
               ) : null}
             </span>
@@ -656,6 +667,7 @@ export default class Status extends ImmutablePureComponent {
               collapsible={settings.getIn(['collapsed', 'enabled'])}
               collapsed={isCollapsed}
               setCollapsed={setCollapsed}
+              directMessage={!!otherAccounts}
             />
           </header>
           <StatusContent
@@ -673,6 +685,7 @@ export default class Status extends ImmutablePureComponent {
               status={status}
               account={status.get('account')}
               showReplyCount={settings.get('show_reply_count')}
+              directMessage={!!otherAccounts}
             />
           ) : null}
           {notification ? (
diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js
index 4c398fd19..85bc4a976 100644
--- a/app/javascript/flavours/glitch/components/status_action_bar.js
+++ b/app/javascript/flavours/glitch/components/status_action_bar.js
@@ -71,6 +71,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
     onBookmark: PropTypes.func,
     withDismiss: PropTypes.bool,
     showReplyCount: PropTypes.bool,
+    directMessage: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
 
@@ -191,7 +192,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
   }
 
   render () {
-    const { status, intl, withDismiss, showReplyCount } = this.props;
+    const { status, intl, withDismiss, showReplyCount, directMessage } = this.props;
 
     const mutingConversation = status.get('muted');
     const anonymousAccess    = !me;
@@ -282,14 +283,15 @@ export default class StatusActionBar extends ImmutablePureComponent {
     return (
       <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' 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} />
-
-        <div className='status__action-bar-dropdown'>
-          <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
-        </div>
+        {!directMessage && [
+          <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' 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} />,
+          <div className='status__action-bar-dropdown'>
+            <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
+          </div>,
+        ]}
 
         <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
       </div>
diff --git a/app/javascript/flavours/glitch/components/status_header.js b/app/javascript/flavours/glitch/components/status_header.js
index f9321904c..23cff286a 100644
--- a/app/javascript/flavours/glitch/components/status_header.js
+++ b/app/javascript/flavours/glitch/components/status_header.js
@@ -6,6 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 //  Mastodon imports.
 import Avatar from './avatar';
 import AvatarOverlay from './avatar_overlay';
+import AvatarComposite from './avatar_composite';
 import DisplayName from './display_name';
 
 export default class StatusHeader extends React.PureComponent {
@@ -14,12 +15,18 @@ export default class StatusHeader extends React.PureComponent {
     status: ImmutablePropTypes.map.isRequired,
     friend: ImmutablePropTypes.map,
     parseClick: PropTypes.func.isRequired,
+    otherAccounts: ImmutablePropTypes.list,
   };
 
   //  Handles clicks on account name/image
+  handleClick = (id, e) => {
+    const { parseClick } = this.props;
+    parseClick(e, `/accounts/${id}`);
+  }
+
   handleAccountClick = (e) => {
-    const { status, parseClick } = this.props;
-    parseClick(e, `/accounts/${status.getIn(['account', 'id'])}`);
+    const { status } = this.props;
+    this.handleClick(status.getIn(['account', 'id']), e);
   }
 
   //  Rendering.
@@ -27,36 +34,55 @@ export default class StatusHeader extends React.PureComponent {
     const {
       status,
       friend,
+      otherAccounts,
     } = this.props;
 
     const account = status.get('account');
 
-    return (
-      <div className='status__info__account' >
-        <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>
-    );
+    let statusAvatar;
+    if (otherAccounts && otherAccounts.size > 0) {
+      statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} onAccountClick={this.handleClick} />;
+    } else if (friend === undefined || friend === null) {
+      statusAvatar = <Avatar account={account} size={48} />;
+    } else {
+      statusAvatar = <AvatarOverlay account={account} friend={friend} />;
+    }
+
+    if (!otherAccounts) {
+      return (
+        <div className='status__info__account'>
+          <a
+            href={account.get('url')}
+            target='_blank'
+            className='status__avatar'
+            onClick={this.handleAccountClick}
+          >
+            {statusAvatar}
+          </a>
+          <a
+            href={account.get('url')}
+            target='_blank'
+            className='status__display-name'
+            onClick={this.handleAccountClick}
+          >
+            <DisplayName account={account} others={otherAccounts} />
+          </a>
+        </div>
+      );
+    } else {
+      // This is a DM conversation
+      return (
+        <div className='status__info__account'>
+          <span className='status__avatar'>
+            {statusAvatar}
+          </span>
+
+          <span className='status__display-name'>
+            <DisplayName account={account} others={otherAccounts} onAccountClick={this.handleClick} />
+          </span>
+        </div>
+      );
+    }
   }
 
 }
diff --git a/app/javascript/flavours/glitch/components/status_icons.js b/app/javascript/flavours/glitch/components/status_icons.js
index c9747650f..4a2c62881 100644
--- a/app/javascript/flavours/glitch/components/status_icons.js
+++ b/app/javascript/flavours/glitch/components/status_icons.js
@@ -22,6 +22,7 @@ export default class StatusIcons extends React.PureComponent {
     mediaIcon: PropTypes.string,
     collapsible: PropTypes.bool,
     collapsed: PropTypes.bool,
+    directMessage: PropTypes.bool,
     setCollapsed: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
   };
@@ -42,6 +43,7 @@ export default class StatusIcons extends React.PureComponent {
       mediaIcon,
       collapsible,
       collapsed,
+      directMessage,
       intl,
     } = this.props;
 
@@ -59,9 +61,7 @@ export default class StatusIcons extends React.PureComponent {
             aria-hidden='true'
           />
         ) : null}
-        {(
-          <VisibilityIcon visibility={status.get('visibility')} />
-        )}
+        {!directMessage && <VisibilityIcon visibility={status.get('visibility')} />}
         {collapsible ? (
           <IconButton
             className='status__collapse-button'