[Glitch] Add edit history to web UI
Port fd3a45e3482e86dad3c1dfc069144864c4ff0b0b to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
		
							parent
							
								
									322e907e04
								
							
						
					
					
						commit
						44b06c4d96
					
				
							
								
								
									
										37
									
								
								app/javascript/flavours/glitch/actions/history.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								app/javascript/flavours/glitch/actions/history.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					import api from 'flavours/glitch/util/api';
 | 
				
			||||||
 | 
					import { importFetchedAccounts } from './importer';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST';
 | 
				
			||||||
 | 
					export const HISTORY_FETCH_SUCCESS = 'HISTORY_FETCH_SUCCESS';
 | 
				
			||||||
 | 
					export const HISTORY_FETCH_FAIL    = 'HISTORY_FETCH_FAIL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fetchHistory = statusId => (dispatch, getState) => {
 | 
				
			||||||
 | 
					  const loading = getState().getIn(['history', statusId, 'loading']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (loading) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  dispatch(fetchHistoryRequest(statusId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
 | 
				
			||||||
 | 
					    dispatch(importFetchedAccounts(data.map(x => x.account)));
 | 
				
			||||||
 | 
					    dispatch(fetchHistorySuccess(statusId, data));
 | 
				
			||||||
 | 
					  }).catch(error => dispatch(fetchHistoryFail(error)));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fetchHistoryRequest = statusId => ({
 | 
				
			||||||
 | 
					  type: HISTORY_FETCH_REQUEST,
 | 
				
			||||||
 | 
					  statusId,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fetchHistorySuccess = (statusId, history) => ({
 | 
				
			||||||
 | 
					  type: HISTORY_FETCH_SUCCESS,
 | 
				
			||||||
 | 
					  statusId,
 | 
				
			||||||
 | 
					  history,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fetchHistoryFail = error => ({
 | 
				
			||||||
 | 
					  type: HISTORY_FETCH_FAIL,
 | 
				
			||||||
 | 
					  error,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -6,6 +6,8 @@ import Overlay from 'react-overlays/lib/Overlay';
 | 
				
			|||||||
import Motion from 'flavours/glitch/util/optional_motion';
 | 
					import Motion from 'flavours/glitch/util/optional_motion';
 | 
				
			||||||
import spring from 'react-motion/lib/spring';
 | 
					import spring from 'react-motion/lib/spring';
 | 
				
			||||||
import { supportsPassiveEvents } from 'detect-passive-events';
 | 
					import { supportsPassiveEvents } from 'detect-passive-events';
 | 
				
			||||||
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					import { CircularProgress } from 'mastodon/components/loading_indicator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
 | 
					const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
 | 
				
			||||||
let id = 0;
 | 
					let id = 0;
 | 
				
			||||||
@ -17,13 +19,18 @@ class DropdownMenu extends React.PureComponent {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static propTypes = {
 | 
					  static propTypes = {
 | 
				
			||||||
    items: PropTypes.array.isRequired,
 | 
					    items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
 | 
				
			||||||
 | 
					    loading: PropTypes.bool,
 | 
				
			||||||
 | 
					    scrollable: PropTypes.bool,
 | 
				
			||||||
    onClose: PropTypes.func.isRequired,
 | 
					    onClose: PropTypes.func.isRequired,
 | 
				
			||||||
    style: PropTypes.object,
 | 
					    style: PropTypes.object,
 | 
				
			||||||
    placement: PropTypes.string,
 | 
					    placement: PropTypes.string,
 | 
				
			||||||
    arrowOffsetLeft: PropTypes.string,
 | 
					    arrowOffsetLeft: PropTypes.string,
 | 
				
			||||||
    arrowOffsetTop: PropTypes.string,
 | 
					    arrowOffsetTop: PropTypes.string,
 | 
				
			||||||
    openedViaKeyboard: PropTypes.bool,
 | 
					    openedViaKeyboard: PropTypes.bool,
 | 
				
			||||||
 | 
					    renderItem: PropTypes.func,
 | 
				
			||||||
 | 
					    renderHeader: PropTypes.func,
 | 
				
			||||||
 | 
					    onItemClick: PropTypes.func.isRequired,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static defaultProps = {
 | 
					  static defaultProps = {
 | 
				
			||||||
@ -45,9 +52,11 @@ class DropdownMenu extends React.PureComponent {
 | 
				
			|||||||
    document.addEventListener('click', this.handleDocumentClick, false);
 | 
					    document.addEventListener('click', this.handleDocumentClick, false);
 | 
				
			||||||
    document.addEventListener('keydown', this.handleKeyDown, false);
 | 
					    document.addEventListener('keydown', this.handleKeyDown, false);
 | 
				
			||||||
    document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
 | 
					    document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.focusedItem && this.props.openedViaKeyboard) {
 | 
					    if (this.focusedItem && this.props.openedViaKeyboard) {
 | 
				
			||||||
      this.focusedItem.focus({ preventScroll: true });
 | 
					      this.focusedItem.focus({ preventScroll: true });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.setState({ mounted: true });
 | 
					    this.setState({ mounted: true });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -66,7 +75,7 @@ class DropdownMenu extends React.PureComponent {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleKeyDown = e => {
 | 
					  handleKeyDown = e => {
 | 
				
			||||||
    const items = Array.from(this.node.getElementsByTagName('a'));
 | 
					    const items = Array.from(this.node.querySelectorAll('a, button'));
 | 
				
			||||||
    const index = items.indexOf(document.activeElement);
 | 
					    const index = items.indexOf(document.activeElement);
 | 
				
			||||||
    let element = null;
 | 
					    let element = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -109,21 +118,11 @@ class DropdownMenu extends React.PureComponent {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleClick = e => {
 | 
					  handleClick = e => {
 | 
				
			||||||
    const i = Number(e.currentTarget.getAttribute('data-index'));
 | 
					    const { onItemClick } = this.props;
 | 
				
			||||||
    const { action, to } = this.props.items[i];
 | 
					    onItemClick(e);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.props.onClose();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (typeof action === 'function') {
 | 
					 | 
				
			||||||
      e.preventDefault();
 | 
					 | 
				
			||||||
      action(e);
 | 
					 | 
				
			||||||
    } else if (to) {
 | 
					 | 
				
			||||||
      e.preventDefault();
 | 
					 | 
				
			||||||
      this.context.router.history.push(to);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  renderItem (option, i) {
 | 
					  renderItem = (option, i) => {
 | 
				
			||||||
    if (option === null) {
 | 
					    if (option === null) {
 | 
				
			||||||
      return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
 | 
					      return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -140,9 +139,11 @@ class DropdownMenu extends React.PureComponent {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
 | 
					    const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props;
 | 
				
			||||||
    const { mounted } = this.state;
 | 
					    const { mounted } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let renderItem = this.props.renderItem || this.renderItem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
 | 
					      <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
 | 
				
			||||||
        {({ opacity, scaleX, scaleY }) => (
 | 
					        {({ opacity, scaleX, scaleY }) => (
 | 
				
			||||||
@ -152,9 +153,23 @@ class DropdownMenu extends React.PureComponent {
 | 
				
			|||||||
          <div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
 | 
					          <div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
 | 
				
			||||||
            <div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
 | 
					            <div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <ul>
 | 
					            <div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })}>
 | 
				
			||||||
              {items.map((option, i) => this.renderItem(option, i))}
 | 
					              {loading && (
 | 
				
			||||||
 | 
					                <CircularProgress size={30} strokeWidth={3.5} />
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              {!loading && renderHeader && (
 | 
				
			||||||
 | 
					                <div className='dropdown-menu__container__header'>
 | 
				
			||||||
 | 
					                  {renderHeader(items)}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              {!loading && (
 | 
				
			||||||
 | 
					                <ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
 | 
				
			||||||
 | 
					                  {items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
 | 
				
			||||||
                </ul>
 | 
					                </ul>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
      </Motion>
 | 
					      </Motion>
 | 
				
			||||||
@ -170,11 +185,14 @@ export default class Dropdown extends React.PureComponent {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static propTypes = {
 | 
					  static propTypes = {
 | 
				
			||||||
    icon: PropTypes.string.isRequired,
 | 
					    children: PropTypes.node,
 | 
				
			||||||
    items: PropTypes.array.isRequired,
 | 
					    icon: PropTypes.string,
 | 
				
			||||||
    size: PropTypes.number.isRequired,
 | 
					    items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
 | 
				
			||||||
 | 
					    loading: PropTypes.bool,
 | 
				
			||||||
 | 
					    size: PropTypes.number,
 | 
				
			||||||
    title: PropTypes.string,
 | 
					    title: PropTypes.string,
 | 
				
			||||||
    disabled: PropTypes.bool,
 | 
					    disabled: PropTypes.bool,
 | 
				
			||||||
 | 
					    scrollable: PropTypes.bool,
 | 
				
			||||||
    status: ImmutablePropTypes.map,
 | 
					    status: ImmutablePropTypes.map,
 | 
				
			||||||
    isUserTouching: PropTypes.func,
 | 
					    isUserTouching: PropTypes.func,
 | 
				
			||||||
    onOpen: PropTypes.func.isRequired,
 | 
					    onOpen: PropTypes.func.isRequired,
 | 
				
			||||||
@ -182,6 +200,9 @@ export default class Dropdown extends React.PureComponent {
 | 
				
			|||||||
    dropdownPlacement: PropTypes.string,
 | 
					    dropdownPlacement: PropTypes.string,
 | 
				
			||||||
    openDropdownId: PropTypes.number,
 | 
					    openDropdownId: PropTypes.number,
 | 
				
			||||||
    openedViaKeyboard: PropTypes.bool,
 | 
					    openedViaKeyboard: PropTypes.bool,
 | 
				
			||||||
 | 
					    renderItem: PropTypes.func,
 | 
				
			||||||
 | 
					    renderHeader: PropTypes.func,
 | 
				
			||||||
 | 
					    onItemClick: PropTypes.func,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static defaultProps = {
 | 
					  static defaultProps = {
 | 
				
			||||||
@ -237,17 +258,21 @@ export default class Dropdown extends React.PureComponent {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleItemClick = e => {
 | 
					  handleItemClick = e => {
 | 
				
			||||||
 | 
					    const { onItemClick } = this.props;
 | 
				
			||||||
    const i = Number(e.currentTarget.getAttribute('data-index'));
 | 
					    const i = Number(e.currentTarget.getAttribute('data-index'));
 | 
				
			||||||
    const { action, to } = this.props.items[i];
 | 
					    const item = this.props.items[i];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.handleClose();
 | 
					    this.handleClose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (typeof action === 'function') {
 | 
					    if (typeof onItemClick === 'function') {
 | 
				
			||||||
      e.preventDefault();
 | 
					      e.preventDefault();
 | 
				
			||||||
      action();
 | 
					      onItemClick(item, i);
 | 
				
			||||||
    } else if (to) {
 | 
					    } else if (item && typeof item.action === 'function') {
 | 
				
			||||||
      e.preventDefault();
 | 
					      e.preventDefault();
 | 
				
			||||||
      this.context.router.history.push(to);
 | 
					      item.action();
 | 
				
			||||||
 | 
					    } else if (item && item.to) {
 | 
				
			||||||
 | 
					      e.preventDefault();
 | 
				
			||||||
 | 
					      this.context.router.history.push(item.to);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -265,12 +290,36 @@ export default class Dropdown extends React.PureComponent {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  close = () => {
 | 
				
			||||||
 | 
					    this.handleClose();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
 | 
					    const {
 | 
				
			||||||
 | 
					      icon,
 | 
				
			||||||
 | 
					      items,
 | 
				
			||||||
 | 
					      size,
 | 
				
			||||||
 | 
					      title,
 | 
				
			||||||
 | 
					      disabled,
 | 
				
			||||||
 | 
					      loading,
 | 
				
			||||||
 | 
					      scrollable,
 | 
				
			||||||
 | 
					      dropdownPlacement,
 | 
				
			||||||
 | 
					      openDropdownId,
 | 
				
			||||||
 | 
					      openedViaKeyboard,
 | 
				
			||||||
 | 
					      children,
 | 
				
			||||||
 | 
					      renderItem,
 | 
				
			||||||
 | 
					      renderHeader,
 | 
				
			||||||
 | 
					    } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const open = this.state.id === openDropdownId;
 | 
					    const open = this.state.id === openDropdownId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    const button = children ? React.cloneElement(React.Children.only(children), {
 | 
				
			||||||
      <div>
 | 
					      ref: this.setTargetRef,
 | 
				
			||||||
 | 
					      onClick: this.handleClick,
 | 
				
			||||||
 | 
					      onMouseDown: this.handleMouseDown,
 | 
				
			||||||
 | 
					      onKeyDown: this.handleButtonKeyDown,
 | 
				
			||||||
 | 
					      onKeyPress: this.handleKeyPress,
 | 
				
			||||||
 | 
					    }) : (
 | 
				
			||||||
      <IconButton
 | 
					      <IconButton
 | 
				
			||||||
        icon={icon}
 | 
					        icon={icon}
 | 
				
			||||||
        title={title}
 | 
					        title={title}
 | 
				
			||||||
@ -283,11 +332,25 @@ export default class Dropdown extends React.PureComponent {
 | 
				
			|||||||
        onKeyDown={this.handleButtonKeyDown}
 | 
					        onKeyDown={this.handleButtonKeyDown}
 | 
				
			||||||
        onKeyPress={this.handleKeyPress}
 | 
					        onKeyPress={this.handleKeyPress}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <React.Fragment>
 | 
				
			||||||
 | 
					        {button}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
 | 
					        <Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
 | 
				
			||||||
          <DropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
 | 
					          <DropdownMenu
 | 
				
			||||||
 | 
					            items={items}
 | 
				
			||||||
 | 
					            loading={loading}
 | 
				
			||||||
 | 
					            scrollable={scrollable}
 | 
				
			||||||
 | 
					            onClose={this.handleClose}
 | 
				
			||||||
 | 
					            openedViaKeyboard={openedViaKeyboard}
 | 
				
			||||||
 | 
					            renderItem={renderItem}
 | 
				
			||||||
 | 
					            renderHeader={renderHeader}
 | 
				
			||||||
 | 
					            onItemClick={this.handleItemClick}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
        </Overlay>
 | 
					        </Overlay>
 | 
				
			||||||
      </div>
 | 
					      </React.Fragment>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dropdown_menu';
 | 
				
			||||||
 | 
					import { fetchHistory } from 'flavours/glitch/actions/history';
 | 
				
			||||||
 | 
					import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapStateToProps = (state, { statusId }) => ({
 | 
				
			||||||
 | 
					  dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
 | 
				
			||||||
 | 
					  openDropdownId: state.getIn(['dropdown_menu', 'openId']),
 | 
				
			||||||
 | 
					  openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
 | 
				
			||||||
 | 
					  items: state.getIn(['history', statusId, 'items']),
 | 
				
			||||||
 | 
					  loading: state.getIn(['history', statusId, 'loading']),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapDispatchToProps = (dispatch, { statusId }) => ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onOpen (id, onItemClick, dropdownPlacement, keyboard) {
 | 
				
			||||||
 | 
					    dispatch(fetchHistory(statusId));
 | 
				
			||||||
 | 
					    dispatch(openDropdownMenu(id, dropdownPlacement, keyboard));
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onClose (id) {
 | 
				
			||||||
 | 
					    dispatch(closeDropdownMenu(id));
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);
 | 
				
			||||||
@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					import { FormattedMessage, injectIntl } from 'react-intl';
 | 
				
			||||||
 | 
					import Icon from 'flavours/glitch/components/icon';
 | 
				
			||||||
 | 
					import DropdownMenu from './containers/dropdown_menu_container';
 | 
				
			||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import { openModal } from 'flavours/glitch/actions/modal';
 | 
				
			||||||
 | 
					import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
 | 
				
			||||||
 | 
					import InlineAccount from 'flavours/glitch/components/inline_account';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapDispatchToProps = (dispatch, { statusId }) => ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onItemClick (index) {
 | 
				
			||||||
 | 
					    dispatch(openModal('COMPARE_HISTORY', { index, statusId }));
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default @connect(null, mapDispatchToProps)
 | 
				
			||||||
 | 
					@injectIntl
 | 
				
			||||||
 | 
					class EditedTimestamp extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static propTypes = {
 | 
				
			||||||
 | 
					    statusId: PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    timestamp: PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    intl: PropTypes.object.isRequired,
 | 
				
			||||||
 | 
					    onItemClick: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleItemClick = (item, i) => {
 | 
				
			||||||
 | 
					    const { onItemClick } = this.props;
 | 
				
			||||||
 | 
					    onItemClick(i);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  renderHeader = items => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <FormattedMessage id='status.edited_x_times' defaultMessage='Edited {count, plural, one {{count} time} other {{count} times}}' values={{ count: items.size - 1 }} />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  renderItem = (item, index, { onClick, onKeyPress }) => {
 | 
				
			||||||
 | 
					    const formattedDate = <RelativeTimestamp timestamp={item.get('created_at')} short={false} />;
 | 
				
			||||||
 | 
					    const formattedName = <InlineAccount accountId={item.get('account')} />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const label = item.get('original') ? (
 | 
				
			||||||
 | 
					      <FormattedMessage id='status.history.created' defaultMessage='{name} created {date}' values={{ name: formattedName, date: formattedDate }} />
 | 
				
			||||||
 | 
					    ) : (
 | 
				
			||||||
 | 
					      <FormattedMessage id='status.history.edited' defaultMessage='{name} edited {date}' values={{ name: formattedName, date: formattedDate }} />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <li className='dropdown-menu__item edited-timestamp__history__item' key={item.get('created_at')}>
 | 
				
			||||||
 | 
					        <button data-index={index} onClick={onClick} onKeyPress={onKeyPress}>{label}</button>
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { timestamp, intl, statusId } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
 | 
				
			||||||
 | 
					        <button className='dropdown-menu__text-button'>
 | 
				
			||||||
 | 
					          <FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' />
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					      </DropdownMenu>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										34
									
								
								app/javascript/flavours/glitch/components/inline_account.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								app/javascript/flavours/glitch/components/inline_account.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import { makeGetAccount } from 'flavours/glitch/selectors';
 | 
				
			||||||
 | 
					import Avatar from 'flavours/glitch/components/avatar';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const makeMapStateToProps = () => {
 | 
				
			||||||
 | 
					  const getAccount = makeGetAccount();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const mapStateToProps = (state, { accountId }) => ({
 | 
				
			||||||
 | 
					    account: getAccount(state, accountId),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return mapStateToProps;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default @connect(makeMapStateToProps)
 | 
				
			||||||
 | 
					class InlineAccount extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static propTypes = {
 | 
				
			||||||
 | 
					    account: ImmutablePropTypes.map.isRequired,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { account } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <span className='inline-account'>
 | 
				
			||||||
 | 
					        <Avatar size={13} account={account} /> <strong>{account.get('username')}</strong>
 | 
				
			||||||
 | 
					      </span>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,10 +1,31 @@
 | 
				
			|||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const CircularProgress = ({ size, strokeWidth }) => {
 | 
				
			||||||
 | 
					  const viewBox = `0 0 ${size} ${size}`;
 | 
				
			||||||
 | 
					  const radius  = (size - strokeWidth) / 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <svg width={size} heigh={size} viewBox={viewBox} className='circular-progress' role='progressbar'>
 | 
				
			||||||
 | 
					      <circle
 | 
				
			||||||
 | 
					        fill='none'
 | 
				
			||||||
 | 
					        cx={size / 2}
 | 
				
			||||||
 | 
					        cy={size / 2}
 | 
				
			||||||
 | 
					        r={radius}
 | 
				
			||||||
 | 
					        strokeWidth={`${strokeWidth}px`}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </svg>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CircularProgress.propTypes = {
 | 
				
			||||||
 | 
					  size: PropTypes.number.isRequired,
 | 
				
			||||||
 | 
					  strokeWidth: PropTypes.number.isRequired,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LoadingIndicator = () => (
 | 
					const LoadingIndicator = () => (
 | 
				
			||||||
  <div className='loading-indicator'>
 | 
					  <div className='loading-indicator'>
 | 
				
			||||||
    <div className='loading-indicator__figure' />
 | 
					    <CircularProgress size={50} strokeWidth={6} />
 | 
				
			||||||
    <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />
 | 
					 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -5,10 +5,15 @@ import PropTypes from 'prop-types';
 | 
				
			|||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  today: { id: 'relative_time.today', defaultMessage: 'today' },
 | 
					  today: { id: 'relative_time.today', defaultMessage: 'today' },
 | 
				
			||||||
  just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
 | 
					  just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
 | 
				
			||||||
 | 
					  just_now_full: { id: 'relative_time.full.just_now', defaultMessage: 'just now' },
 | 
				
			||||||
  seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
 | 
					  seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
 | 
				
			||||||
 | 
					  seconds_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' },
 | 
				
			||||||
  minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
 | 
					  minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
 | 
				
			||||||
 | 
					  minutes_full: { id: 'relative_time.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago' },
 | 
				
			||||||
  hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
 | 
					  hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
 | 
				
			||||||
 | 
					  hours_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' },
 | 
				
			||||||
  days: { id: 'relative_time.days', defaultMessage: '{number}d' },
 | 
					  days: { id: 'relative_time.days', defaultMessage: '{number}d' },
 | 
				
			||||||
 | 
					  days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' },
 | 
				
			||||||
  moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
 | 
					  moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
 | 
				
			||||||
  seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
 | 
					  seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
 | 
				
			||||||
  minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
 | 
					  minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
 | 
				
			||||||
@ -66,7 +71,7 @@ const getUnitDelay = units => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const timeAgoString = (intl, date, now, year, timeGiven = true) => {
 | 
					export const timeAgoString = (intl, date, now, year, timeGiven, short) => {
 | 
				
			||||||
  const delta = now - date.getTime();
 | 
					  const delta = now - date.getTime();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let relativeTime;
 | 
					  let relativeTime;
 | 
				
			||||||
@ -74,16 +79,16 @@ export const timeAgoString = (intl, date, now, year, timeGiven = true) => {
 | 
				
			|||||||
  if (delta < DAY && !timeGiven) {
 | 
					  if (delta < DAY && !timeGiven) {
 | 
				
			||||||
    relativeTime = intl.formatMessage(messages.today);
 | 
					    relativeTime = intl.formatMessage(messages.today);
 | 
				
			||||||
  } else if (delta < 10 * SECOND) {
 | 
					  } else if (delta < 10 * SECOND) {
 | 
				
			||||||
    relativeTime = intl.formatMessage(messages.just_now);
 | 
					    relativeTime = intl.formatMessage(short ? messages.just_now : messages.just_now_full);
 | 
				
			||||||
  } else if (delta < 7 * DAY) {
 | 
					  } else if (delta < 7 * DAY) {
 | 
				
			||||||
    if (delta < MINUTE) {
 | 
					    if (delta < MINUTE) {
 | 
				
			||||||
      relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
 | 
					      relativeTime = intl.formatMessage(short ? messages.seconds : messages.seconds_full, { number: Math.floor(delta / SECOND) });
 | 
				
			||||||
    } else if (delta < HOUR) {
 | 
					    } else if (delta < HOUR) {
 | 
				
			||||||
      relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
 | 
					      relativeTime = intl.formatMessage(short ? messages.minutes : messages.minutes_full, { number: Math.floor(delta / MINUTE) });
 | 
				
			||||||
    } else if (delta < DAY) {
 | 
					    } else if (delta < DAY) {
 | 
				
			||||||
      relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
 | 
					      relativeTime = intl.formatMessage(short ? messages.hours : messages.hours_full, { number: Math.floor(delta / HOUR) });
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
 | 
					      relativeTime = intl.formatMessage(short ? messages.days : messages.days_full, { number: Math.floor(delta / DAY) });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } else if (date.getFullYear() === year) {
 | 
					  } else if (date.getFullYear() === year) {
 | 
				
			||||||
    relativeTime = intl.formatDate(date, shortDateFormatOptions);
 | 
					    relativeTime = intl.formatDate(date, shortDateFormatOptions);
 | 
				
			||||||
@ -124,6 +129,7 @@ class RelativeTimestamp extends React.Component {
 | 
				
			|||||||
    timestamp: PropTypes.string.isRequired,
 | 
					    timestamp: PropTypes.string.isRequired,
 | 
				
			||||||
    year: PropTypes.number.isRequired,
 | 
					    year: PropTypes.number.isRequired,
 | 
				
			||||||
    futureDate: PropTypes.bool,
 | 
					    futureDate: PropTypes.bool,
 | 
				
			||||||
 | 
					    short: PropTypes.bool,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  state = {
 | 
					  state = {
 | 
				
			||||||
@ -132,6 +138,7 @@ class RelativeTimestamp extends React.Component {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  static defaultProps = {
 | 
					  static defaultProps = {
 | 
				
			||||||
    year: (new Date()).getFullYear(),
 | 
					    year: (new Date()).getFullYear(),
 | 
				
			||||||
 | 
					    short: true,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  shouldComponentUpdate (nextProps, nextState) {
 | 
					  shouldComponentUpdate (nextProps, nextState) {
 | 
				
			||||||
@ -176,11 +183,11 @@ class RelativeTimestamp extends React.Component {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { timestamp, intl, year, futureDate } = this.props;
 | 
					    const { timestamp, intl, year, futureDate, short } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const timeGiven    = timestamp.includes('T');
 | 
					    const timeGiven    = timestamp.includes('T');
 | 
				
			||||||
    const date         = new Date(timestamp);
 | 
					    const date         = new Date(timestamp);
 | 
				
			||||||
    const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven);
 | 
					    const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven, short);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
 | 
					      <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ import StatusContent from 'flavours/glitch/components/status_content';
 | 
				
			|||||||
import MediaGallery from 'flavours/glitch/components/media_gallery';
 | 
					import MediaGallery from 'flavours/glitch/components/media_gallery';
 | 
				
			||||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
 | 
					import AttachmentList from 'flavours/glitch/components/attachment_list';
 | 
				
			||||||
import { Link } from 'react-router-dom';
 | 
					import { Link } from 'react-router-dom';
 | 
				
			||||||
import { injectIntl, FormattedDate, FormattedMessage } from 'react-intl';
 | 
					import { injectIntl, FormattedDate } from 'react-intl';
 | 
				
			||||||
import Card from './card';
 | 
					import Card from './card';
 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
				
			||||||
import Video from 'flavours/glitch/features/video';
 | 
					import Video from 'flavours/glitch/features/video';
 | 
				
			||||||
@ -19,6 +19,7 @@ import PollContainer from 'flavours/glitch/containers/poll_container';
 | 
				
			|||||||
import Icon from 'flavours/glitch/components/icon';
 | 
					import Icon from 'flavours/glitch/components/icon';
 | 
				
			||||||
import AnimatedNumber from 'flavours/glitch/components/animated_number';
 | 
					import AnimatedNumber from 'flavours/glitch/components/animated_number';
 | 
				
			||||||
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
 | 
					import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
 | 
				
			||||||
 | 
					import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default @injectIntl
 | 
					export default @injectIntl
 | 
				
			||||||
class DetailedStatus extends ImmutablePureComponent {
 | 
					class DetailedStatus extends ImmutablePureComponent {
 | 
				
			||||||
@ -265,7 +266,7 @@ class DetailedStatus extends ImmutablePureComponent {
 | 
				
			|||||||
      edited = (
 | 
					      edited = (
 | 
				
			||||||
        <React.Fragment>
 | 
					        <React.Fragment>
 | 
				
			||||||
          <React.Fragment> · </React.Fragment>
 | 
					          <React.Fragment> · </React.Fragment>
 | 
				
			||||||
          <FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(status.get('edited_at'), { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} />
 | 
					          <EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
 | 
				
			||||||
        </React.Fragment>
 | 
					        </React.Fragment>
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					import { closeModal } from 'flavours/glitch/actions/modal';
 | 
				
			||||||
 | 
					import emojify from 'flavours/glitch/util/emoji';
 | 
				
			||||||
 | 
					import escapeTextContentForBrowser from 'escape-html';
 | 
				
			||||||
 | 
					import InlineAccount from 'flavours/glitch/components/inline_account';
 | 
				
			||||||
 | 
					import IconButton from 'flavours/glitch/components/icon_button';
 | 
				
			||||||
 | 
					import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapStateToProps = (state, { statusId }) => ({
 | 
				
			||||||
 | 
					  versions: state.getIn(['history', statusId, 'items']),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onClose() {
 | 
				
			||||||
 | 
					    dispatch(closeModal());
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default @connect(mapStateToProps, mapDispatchToProps)
 | 
				
			||||||
 | 
					class CompareHistoryModal extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static propTypes = {
 | 
				
			||||||
 | 
					    onClose: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    index: PropTypes.number.isRequired,
 | 
				
			||||||
 | 
					    statusId: PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    versions: ImmutablePropTypes.list.isRequired,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { index, versions, onClose } = this.props;
 | 
				
			||||||
 | 
					    const currentVersion = versions.get(index);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => {
 | 
				
			||||||
 | 
					      obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
 | 
				
			||||||
 | 
					      return obj;
 | 
				
			||||||
 | 
					    }, {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const content = { __html: emojify(currentVersion.get('content'), emojiMap) };
 | 
				
			||||||
 | 
					    const spoilerContent = { __html: emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap) };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
 | 
				
			||||||
 | 
					    const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const label = currentVersion.get('original') ? (
 | 
				
			||||||
 | 
					      <FormattedMessage id='status.history.created' defaultMessage='{name} created {date}' values={{ name: formattedName, date: formattedDate }} />
 | 
				
			||||||
 | 
					    ) : (
 | 
				
			||||||
 | 
					      <FormattedMessage id='status.history.edited' defaultMessage='{name} edited {date}' values={{ name: formattedName, date: formattedDate }} />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className='modal-root__modal compare-history-modal'>
 | 
				
			||||||
 | 
					        <div className='report-modal__target'>
 | 
				
			||||||
 | 
					          <IconButton className='report-modal__close' icon='times' onClick={onClose} size={20} />
 | 
				
			||||||
 | 
					          {label}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className='compare-history-modal__container'>
 | 
				
			||||||
 | 
					          <div className='status__content'>
 | 
				
			||||||
 | 
					            {currentVersion.get('spoiler_text').length > 0 && (
 | 
				
			||||||
 | 
					              <React.Fragment>
 | 
				
			||||||
 | 
					                <div className='translate' dangerouslySetInnerHTML={spoilerContent} />
 | 
				
			||||||
 | 
					                <hr />
 | 
				
			||||||
 | 
					              </React.Fragment>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -24,6 +24,7 @@ import {
 | 
				
			|||||||
  ListEditor,
 | 
					  ListEditor,
 | 
				
			||||||
  ListAdder,
 | 
					  ListAdder,
 | 
				
			||||||
  PinnedAccountsEditor,
 | 
					  PinnedAccountsEditor,
 | 
				
			||||||
 | 
					  CompareHistoryModal,
 | 
				
			||||||
} from 'flavours/glitch/util/async-components';
 | 
					} from 'flavours/glitch/util/async-components';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MODAL_COMPONENTS = {
 | 
					const MODAL_COMPONENTS = {
 | 
				
			||||||
@ -42,9 +43,10 @@ const MODAL_COMPONENTS = {
 | 
				
			|||||||
  'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
 | 
					  'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
 | 
				
			||||||
  'EMBED': EmbedModal,
 | 
					  'EMBED': EmbedModal,
 | 
				
			||||||
  'LIST_EDITOR': ListEditor,
 | 
					  'LIST_EDITOR': ListEditor,
 | 
				
			||||||
  'LIST_ADDER':ListAdder,
 | 
					 | 
				
			||||||
  'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
 | 
					  'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
 | 
				
			||||||
 | 
					  'LIST_ADDER': ListAdder,
 | 
				
			||||||
  'PINNED_ACCOUNTS_EDITOR': PinnedAccountsEditor,
 | 
					  'PINNED_ACCOUNTS_EDITOR': PinnedAccountsEditor,
 | 
				
			||||||
 | 
					  'COMPARE_HISTORY': CompareHistoryModal,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class ModalRoot extends React.PureComponent {
 | 
					export default class ModalRoot extends React.PureComponent {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										28
									
								
								app/javascript/flavours/glitch/reducers/history.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/javascript/flavours/glitch/reducers/history.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					import { HISTORY_FETCH_REQUEST, HISTORY_FETCH_SUCCESS, HISTORY_FETCH_FAIL } from 'flavours/glitch/actions/history';
 | 
				
			||||||
 | 
					import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const initialHistory = ImmutableMap({
 | 
				
			||||||
 | 
					  loading: false,
 | 
				
			||||||
 | 
					  items: ImmutableList(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const initialState = ImmutableMap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function history(state = initialState, action) {
 | 
				
			||||||
 | 
					  switch(action.type) {
 | 
				
			||||||
 | 
					  case HISTORY_FETCH_REQUEST:
 | 
				
			||||||
 | 
					    return state.update(action.statusId, initialHistory, history => history.withMutations(map => {
 | 
				
			||||||
 | 
					      map.set('loading', true);
 | 
				
			||||||
 | 
					      map.set('items', ImmutableList());
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  case HISTORY_FETCH_SUCCESS:
 | 
				
			||||||
 | 
					    return state.update(action.statusId, initialHistory, history => history.withMutations(map => {
 | 
				
			||||||
 | 
					      map.set('loading', false);
 | 
				
			||||||
 | 
					      map.set('items', fromJS(action.history.map((x, i) => ({ ...x, account: x.account.id, original: i === 0 })).reverse()));
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  case HISTORY_FETCH_FAIL:
 | 
				
			||||||
 | 
					    return state.update(action.statusId, initialHistory, history => history.set('loading', false));
 | 
				
			||||||
 | 
					  default:
 | 
				
			||||||
 | 
					    return state;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -41,6 +41,7 @@ import markers from './markers';
 | 
				
			|||||||
import account_notes from './account_notes';
 | 
					import account_notes from './account_notes';
 | 
				
			||||||
import picture_in_picture from './picture_in_picture';
 | 
					import picture_in_picture from './picture_in_picture';
 | 
				
			||||||
import accounts_map from './accounts_map';
 | 
					import accounts_map from './accounts_map';
 | 
				
			||||||
 | 
					import history from './history';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const reducers = {
 | 
					const reducers = {
 | 
				
			||||||
  announcements,
 | 
					  announcements,
 | 
				
			||||||
@ -85,6 +86,7 @@ const reducers = {
 | 
				
			|||||||
  markers,
 | 
					  markers,
 | 
				
			||||||
  account_notes,
 | 
					  account_notes,
 | 
				
			||||||
  picture_in_picture,
 | 
					  picture_in_picture,
 | 
				
			||||||
 | 
					  history,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default combineReducers(reducers);
 | 
					export default combineReducers(reducers);
 | 
				
			||||||
 | 
				
			|||||||
@ -500,8 +500,47 @@
 | 
				
			|||||||
  box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
 | 
					  box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
 | 
				
			||||||
  z-index: 9999;
 | 
					  z-index: 9999;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ul {
 | 
					  &__text-button {
 | 
				
			||||||
 | 
					    display: inline;
 | 
				
			||||||
 | 
					    color: inherit;
 | 
				
			||||||
 | 
					    background: transparent;
 | 
				
			||||||
 | 
					    border: 0;
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					    font-family: inherit;
 | 
				
			||||||
 | 
					    font-size: inherit;
 | 
				
			||||||
 | 
					    line-height: inherit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:focus {
 | 
				
			||||||
 | 
					      outline: 1px dotted;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &__container {
 | 
				
			||||||
 | 
					    &__header {
 | 
				
			||||||
 | 
					      border-bottom: 1px solid darken($ui-secondary-color, 8%);
 | 
				
			||||||
 | 
					      padding: 4px 14px;
 | 
				
			||||||
 | 
					      padding-bottom: 8px;
 | 
				
			||||||
 | 
					      font-size: 13px;
 | 
				
			||||||
 | 
					      line-height: 18px;
 | 
				
			||||||
 | 
					      color: $inverted-text-color;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__list {
 | 
				
			||||||
      list-style: none;
 | 
					      list-style: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &--scrollable {
 | 
				
			||||||
 | 
					        max-height: 300px;
 | 
				
			||||||
 | 
					        overflow-y: scroll;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &--loading {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					      padding: 30px 45px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -541,18 +580,29 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dropdown-menu__item {
 | 
					.dropdown-menu__item {
 | 
				
			||||||
  a {
 | 
					 | 
				
			||||||
  font-size: 13px;
 | 
					  font-size: 13px;
 | 
				
			||||||
  line-height: 18px;
 | 
					  line-height: 18px;
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
 | 
					  color: $inverted-text-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  a,
 | 
				
			||||||
 | 
					  button {
 | 
				
			||||||
 | 
					    font-family: inherit;
 | 
				
			||||||
 | 
					    font-size: inherit;
 | 
				
			||||||
 | 
					    line-height: inherit;
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
    padding: 4px 14px;
 | 
					    padding: 4px 14px;
 | 
				
			||||||
 | 
					    border: 0;
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
    box-sizing: border-box;
 | 
					    box-sizing: border-box;
 | 
				
			||||||
    text-decoration: none;
 | 
					    text-decoration: none;
 | 
				
			||||||
    background: $ui-secondary-color;
 | 
					    background: $ui-secondary-color;
 | 
				
			||||||
    color: $inverted-text-color;
 | 
					    color: inherit;
 | 
				
			||||||
    overflow: hidden;
 | 
					    overflow: hidden;
 | 
				
			||||||
    text-overflow: ellipsis;
 | 
					    text-overflow: ellipsis;
 | 
				
			||||||
    white-space: nowrap;
 | 
					    white-space: nowrap;
 | 
				
			||||||
 | 
					    text-align: inherit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &:focus,
 | 
					    &:focus,
 | 
				
			||||||
    &:hover,
 | 
					    &:hover,
 | 
				
			||||||
@ -564,6 +614,42 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dropdown-menu__item--text {
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  text-overflow: ellipsis;
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					  padding: 4px 14px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dropdown-menu__item.edited-timestamp__history__item {
 | 
				
			||||||
 | 
					  border-bottom: 1px solid darken($ui-secondary-color, 8%);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:last-child {
 | 
				
			||||||
 | 
					    border-bottom: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &.dropdown-menu__item--text,
 | 
				
			||||||
 | 
					  a,
 | 
				
			||||||
 | 
					  button {
 | 
				
			||||||
 | 
					    padding: 8px 14px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.inline-account {
 | 
				
			||||||
 | 
					  display: inline-flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  vertical-align: top;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .account__avatar {
 | 
				
			||||||
 | 
					    margin-right: 5px;
 | 
				
			||||||
 | 
					    border-radius: 50%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  strong {
 | 
				
			||||||
 | 
					    font-weight: 600;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dropdown--active .dropdown__content {
 | 
					.dropdown--active .dropdown__content {
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  line-height: 18px;
 | 
					  line-height: 18px;
 | 
				
			||||||
@ -1229,36 +1315,48 @@ button.icon-button.active i.fa-retweet {
 | 
				
			|||||||
  top: 50%;
 | 
					  top: 50%;
 | 
				
			||||||
  left: 50%;
 | 
					  left: 50%;
 | 
				
			||||||
  transform: translate(-50%, -50%);
 | 
					  transform: translate(-50%, -50%);
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  span {
 | 
					.circular-progress {
 | 
				
			||||||
    display: block;
 | 
					  color: lighten($ui-base-color, 26%);
 | 
				
			||||||
    float: left;
 | 
					  animation: 1.4s linear 0s infinite normal none running simple-rotate;
 | 
				
			||||||
    transform: translateX(-50%);
 | 
					
 | 
				
			||||||
    margin: 82px 0 0 50%;
 | 
					  circle {
 | 
				
			||||||
    white-space: nowrap;
 | 
					    stroke: currentColor;
 | 
				
			||||||
 | 
					    stroke-dasharray: 80px, 200px;
 | 
				
			||||||
 | 
					    stroke-dashoffset: 0;
 | 
				
			||||||
 | 
					    animation: circular-progress 1.4s ease-in-out infinite;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.loading-indicator__figure {
 | 
					@keyframes circular-progress {
 | 
				
			||||||
  position: absolute;
 | 
					  0% {
 | 
				
			||||||
  top: 50%;
 | 
					    stroke-dasharray: 1px, 200px;
 | 
				
			||||||
  left: 50%;
 | 
					    stroke-dashoffset: 0;
 | 
				
			||||||
  transform: translate(-50%, -50%);
 | 
					 | 
				
			||||||
  width: 42px;
 | 
					 | 
				
			||||||
  height: 42px;
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
  background-color: transparent;
 | 
					 | 
				
			||||||
  border: 0 solid lighten($ui-base-color, 26%);
 | 
					 | 
				
			||||||
  border-width: 6px;
 | 
					 | 
				
			||||||
  border-radius: 50%;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.no-reduce-motion .loading-indicator span {
 | 
					  50% {
 | 
				
			||||||
  animation: loader-label 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000);
 | 
					    stroke-dasharray: 100px, 200px;
 | 
				
			||||||
 | 
					    stroke-dashoffset: -15px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.no-reduce-motion .loading-indicator__figure {
 | 
					  100% {
 | 
				
			||||||
  animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000);
 | 
					    stroke-dasharray: 100px, 200px;
 | 
				
			||||||
 | 
					    stroke-dashoffset: -125px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes simple-rotate {
 | 
				
			||||||
 | 
					  0% {
 | 
				
			||||||
 | 
					    transform: rotate(0deg);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  100% {
 | 
				
			||||||
 | 
					    transform: rotate(360deg);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@keyframes spring-rotate-in {
 | 
					@keyframes spring-rotate-in {
 | 
				
			||||||
@ -1305,40 +1403,6 @@ button.icon-button.active i.fa-retweet {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@keyframes loader-figure {
 | 
					 | 
				
			||||||
  0% {
 | 
					 | 
				
			||||||
    width: 0;
 | 
					 | 
				
			||||||
    height: 0;
 | 
					 | 
				
			||||||
    background-color: lighten($ui-base-color, 26%);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  29% {
 | 
					 | 
				
			||||||
    background-color: lighten($ui-base-color, 26%);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  30% {
 | 
					 | 
				
			||||||
    width: 42px;
 | 
					 | 
				
			||||||
    height: 42px;
 | 
					 | 
				
			||||||
    background-color: transparent;
 | 
					 | 
				
			||||||
    border-width: 21px;
 | 
					 | 
				
			||||||
    opacity: 1;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  100% {
 | 
					 | 
				
			||||||
    width: 42px;
 | 
					 | 
				
			||||||
    height: 42px;
 | 
					 | 
				
			||||||
    border-width: 0;
 | 
					 | 
				
			||||||
    opacity: 0;
 | 
					 | 
				
			||||||
    background-color: transparent;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@keyframes loader-label {
 | 
					 | 
				
			||||||
  0% { opacity: 0.25; }
 | 
					 | 
				
			||||||
  30% { opacity: 1; }
 | 
					 | 
				
			||||||
  100% { opacity: 0.25; }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.spoiler-button {
 | 
					.spoiler-button {
 | 
				
			||||||
  top: 0;
 | 
					  top: 0;
 | 
				
			||||||
  left: 0;
 | 
					  left: 0;
 | 
				
			||||||
@ -1508,6 +1572,41 @@ button.icon-button.active i.fa-retweet {
 | 
				
			|||||||
  filter: none;
 | 
					  filter: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.compare-history-modal {
 | 
				
			||||||
 | 
					  .report-modal__target {
 | 
				
			||||||
 | 
					    border-bottom: 1px solid $ui-secondary-color;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &__container {
 | 
				
			||||||
 | 
					    padding: 30px;
 | 
				
			||||||
 | 
					    pointer-events: all;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .status__content {
 | 
				
			||||||
 | 
					    color: $inverted-text-color;
 | 
				
			||||||
 | 
					    font-size: 19px;
 | 
				
			||||||
 | 
					    line-height: 24px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .emojione {
 | 
				
			||||||
 | 
					      width: 24px;
 | 
				
			||||||
 | 
					      height: 24px;
 | 
				
			||||||
 | 
					      margin: -1px 0 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    a {
 | 
				
			||||||
 | 
					      color: $highlight-text-color;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    hr {
 | 
				
			||||||
 | 
					      height: 0.25rem;
 | 
				
			||||||
 | 
					      padding: 0;
 | 
				
			||||||
 | 
					      background-color: $ui-secondary-color;
 | 
				
			||||||
 | 
					      border: 0;
 | 
				
			||||||
 | 
					      margin: 20px 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.loading-bar {
 | 
					.loading-bar {
 | 
				
			||||||
  background-color: $ui-highlight-color;
 | 
					  background-color: $ui-highlight-color;
 | 
				
			||||||
  height: 3px;
 | 
					  height: 3px;
 | 
				
			||||||
 | 
				
			|||||||
@ -420,7 +420,8 @@
 | 
				
			|||||||
.report-modal,
 | 
					.report-modal,
 | 
				
			||||||
.actions-modal,
 | 
					.actions-modal,
 | 
				
			||||||
.mute-modal,
 | 
					.mute-modal,
 | 
				
			||||||
.block-modal {
 | 
					.block-modal,
 | 
				
			||||||
 | 
					.compare-history-modal {
 | 
				
			||||||
  background: lighten($ui-secondary-color, 8%);
 | 
					  background: lighten($ui-secondary-color, 8%);
 | 
				
			||||||
  color: $inverted-text-color;
 | 
					  color: $inverted-text-color;
 | 
				
			||||||
  border-radius: 8px;
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
				
			|||||||
@ -173,3 +173,7 @@ export function Directory () {
 | 
				
			|||||||
export function FollowRecommendations () {
 | 
					export function FollowRecommendations () {
 | 
				
			||||||
  return import(/* webpackChunkName: "features/glitch/async/follow_recommendations" */'flavours/glitch/features/follow_recommendations');
 | 
					  return import(/* webpackChunkName: "features/glitch/async/follow_recommendations" */'flavours/glitch/features/follow_recommendations');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function CompareHistoryModal () {
 | 
				
			||||||
 | 
					  return import(/*webpackChunkName: "flavours/glitch/async/compare_history_modal" */'flavours/glitch/features/ui/components/compare_history_modal');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user