fix account admin info, and links in the footer on the left

This commit is contained in:
Baptiste Lemoine 2019-12-18 12:52:46 +01:00
parent 626fa25f4b
commit 4e336c8e6b
13 changed files with 1041 additions and 576 deletions

View File

@ -12,7 +12,7 @@ import classNames from 'classnames';
const textAtCursorMatchesToken = (str, caretPosition) => { const textAtCursorMatchesToken = (str, caretPosition) => {
let word; let word;
let left = str.slice(0, caretPosition).search(/\S+$/); let left = str.slice(0, caretPosition).search(/\S+$/);
let right = str.slice(caretPosition).search(/\s/); let right = str.slice(caretPosition).search(/\s/);
if (right < 0) { if (right < 0) {
@ -37,34 +37,37 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
export default class AutosuggestTextarea extends ImmutablePureComponent { export default class AutosuggestTextarea extends ImmutablePureComponent {
static propTypes = { static propTypes = {
value: PropTypes.string, value : PropTypes.string,
suggestions: ImmutablePropTypes.list, suggestions : ImmutablePropTypes.list,
disabled: PropTypes.bool, disabled : PropTypes.bool,
placeholder: PropTypes.string, placeholder : PropTypes.string,
onSuggestionSelected: PropTypes.func.isRequired, onSuggestionSelected : PropTypes.func.isRequired,
onSuggestionsClearRequested: PropTypes.func.isRequired, onSuggestionsClearRequested: PropTypes.func.isRequired,
onSuggestionsFetchRequested: PropTypes.func.isRequired, onSuggestionsFetchRequested: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange : PropTypes.func.isRequired,
onKeyUp: PropTypes.func, onKeyUp : PropTypes.func,
onKeyDown: PropTypes.func, onKeyDown : PropTypes.func,
onPaste: PropTypes.func.isRequired, onPaste : PropTypes.func.isRequired,
autoFocus: PropTypes.bool, autoFocus : PropTypes.bool,
directMessage : PropTypes.bool,
directMessageRecipient : PropTypes.string,
}; };
static defaultProps = { static defaultProps = {
autoFocus: true, autoFocus : true,
directMessage: false,
}; };
state = { state = {
suggestionsHidden: true, suggestionsHidden : true,
focused: false, focused : false,
selectedSuggestion: 0, selectedSuggestion: 0,
lastToken: null, lastToken : null,
tokenStart: 0, tokenStart : 0,
}; };
onChange = (e) => { onChange = (e) => {
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart); const [tokenStart, token] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
if (token !== null && this.state.lastToken !== token) { if (token !== null && this.state.lastToken !== token) {
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
@ -75,7 +78,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
} }
this.props.onChange(e); this.props.onChange(e);
} };
onKeyDown = (e) => { onKeyDown = (e) => {
const { suggestions, disabled } = this.props; const { suggestions, disabled } = this.props;
@ -92,7 +95,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
return; return;
} }
switch(e.key) { switch (e.key) {
case 'Escape': case 'Escape':
if (suggestions.size === 0 || suggestionsHidden) { if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector('.ui').parentElement.focus(); document.querySelector('.ui').parentElement.focus();
@ -124,7 +127,6 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
e.stopPropagation(); e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
} }
break; break;
} }
@ -133,27 +135,27 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
} }
this.props.onKeyDown(e); this.props.onKeyDown(e);
} };
onBlur = () => { onBlur = () => {
this.setState({ suggestionsHidden: true, focused: false }); this.setState({ suggestionsHidden: true, focused: false });
} };
onFocus = (e) => { onFocus = (e) => {
this.setState({ focused: true }); this.setState({ focused: true });
if (this.props.onFocus) { if (this.props.onFocus) {
this.props.onFocus(e); this.props.onFocus(e);
} }
} };
onSuggestionClick = (e) => { onSuggestionClick = (e) => {
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
e.preventDefault(); e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.textarea.focus(); this.textarea.focus();
} };
componentWillReceiveProps (nextProps) { componentWillReceiveProps(nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false }); this.setState({ suggestionsHidden: false });
} }
@ -161,14 +163,14 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
setTextarea = (c) => { setTextarea = (c) => {
this.textarea = c; this.textarea = c;
} };
onPaste = (e) => { onPaste = (e) => {
if (e.clipboardData && e.clipboardData.files.length === 1) { if (e.clipboardData && e.clipboardData.files.length === 1) {
this.props.onPaste(e.clipboardData.files); this.props.onPaste(e.clipboardData.files);
e.preventDefault(); e.preventDefault();
} }
} };
renderSuggestion = (suggestion, i) => { renderSuggestion = (suggestion, i) => {
const { selectedSuggestion } = this.state; const { selectedSuggestion } = this.state;
@ -176,23 +178,30 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
if (suggestion.type === 'emoji') { if (suggestion.type === 'emoji') {
inner = <AutosuggestEmoji emoji={suggestion} />; inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id; key = suggestion.id;
} else if (suggestion.type === 'hashtag') { } else if (suggestion.type === 'hashtag') {
inner = <AutosuggestHashtag tag={suggestion} />; inner = <AutosuggestHashtag tag={suggestion} />;
key = suggestion.name; key = suggestion.name;
} else if (suggestion.type === 'account') { } else if (suggestion.type === 'account') {
inner = <AutosuggestAccountContainer id={suggestion.id} />; inner = <AutosuggestAccountContainer id={suggestion.id} />;
key = suggestion.id; key = suggestion.id;
} }
return ( return (
<div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}> <div
role='button'
tabIndex='0'
key={key}
data-index={i}
className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })}
onMouseDown={this.onSuggestionClick}
>
{inner} {inner}
</div> </div >
); );
} };
render () { render() {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props; const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props;
const { suggestionsHidden } = this.state; const { suggestionsHidden } = this.state;
const style = { direction: 'ltr' }; const style = { direction: 'ltr' };
@ -202,10 +211,13 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
} }
return [ return [
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'> <div
className='compose-form__autosuggest-wrapper'
key='autosuggest-wrapper'
>
<div className='autosuggest-textarea'> <div className='autosuggest-textarea'>
<label> <label >
<span style={{ display: 'none' }}>{placeholder}</span> <span style={{ display: 'none' }}>{placeholder}</span >
<Textarea <Textarea
inputRef={this.setTextarea} inputRef={this.setTextarea}
@ -223,16 +235,21 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
style={style} style={style}
aria-autocomplete='list' aria-autocomplete='list'
/> />
</label> </label >
</div> </div >
{children} {children}
</div>, </div >,
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'> <div
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> className='autosuggest-textarea__suggestions-wrapper'
key='suggestions-wrapper'
>
<div
className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}
>
{suggestions.map(this.renderSuggestion)} {suggestions.map(this.renderSuggestion)}
</div> </div >
</div>, </div >,
]; ];
} }

View File

@ -2,13 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, show : { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, hide : { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' }, moveLeft : { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
}); });
@ -20,19 +20,19 @@ class ColumnHeader extends React.PureComponent {
}; };
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
title: PropTypes.node, title : PropTypes.node,
icon: PropTypes.string, icon : PropTypes.string,
active: PropTypes.bool, active : PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn : PropTypes.bool,
extraButton: PropTypes.node, extraButton : PropTypes.node,
showBackButton: PropTypes.bool, showBackButton: PropTypes.bool,
children: PropTypes.node, children : PropTypes.node,
pinned: PropTypes.bool, pinned : PropTypes.bool,
placeholder: PropTypes.bool, placeholder : PropTypes.bool,
onPin: PropTypes.func, onPin : PropTypes.func,
onMove: PropTypes.func, onMove : PropTypes.func,
onClick: PropTypes.func, onClick : PropTypes.func,
}; };
state = { state = {
@ -46,41 +46,41 @@ class ColumnHeader extends React.PureComponent {
} else { } else {
this.context.router.history.goBack(); this.context.router.history.goBack();
} }
} };
handleToggleClick = (e) => { handleToggleClick = (e) => {
e.stopPropagation(); e.stopPropagation();
this.setState({ collapsed: !this.state.collapsed, animating: true }); this.setState({ collapsed: !this.state.collapsed, animating: true });
} };
handleTitleClick = () => { handleTitleClick = () => {
this.props.onClick(); this.props.onClick();
} };
handleMoveLeft = () => { handleMoveLeft = () => {
this.props.onMove(-1); this.props.onMove(-1);
} };
handleMoveRight = () => { handleMoveRight = () => {
this.props.onMove(1); this.props.onMove(1);
} };
handleBackClick = () => { handleBackClick = () => {
this.historyBack(); this.historyBack();
} };
handleTransitionEnd = () => { handleTransitionEnd = () => {
this.setState({ animating: false }); this.setState({ animating: false });
} };
handlePin = () => { handlePin = () => {
if (!this.props.pinned) { if (!this.props.pinned) {
this.historyBack(); this.historyBack();
} }
this.props.onPin(); this.props.onPin();
} };
render () { render() {
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder } = this.props; const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder } = this.props;
const { collapsed, animating } = this.state; const { collapsed, animating } = this.state;
@ -105,31 +105,82 @@ class ColumnHeader extends React.PureComponent {
if (children) { if (children) {
extraContent = ( extraContent = (
<div key='extra-content' className='column-header__collapsible__extra'> <div
key='extra-content'
className='column-header__collapsible__extra'
>
{children} {children}
</div> </div >
); );
} }
if (multiColumn && pinned) { if (multiColumn && pinned) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>; pinButton =
(<button
key='pin-button'
className='text-btn column-header__setting-btn'
onClick={this.handlePin}
><Icon
id='times'
/> <FormattedMessage
id='column_header.unpin'
defaultMessage='Unpin'
/></button >);
moveButtons = ( moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'> <div
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button> key='move-buttons'
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button> className='column-header__setting-arrows'
</div> >
<button
title={formatMessage(messages.moveLeft)}
aria-label={formatMessage(messages.moveLeft)}
className='text-btn column-header__setting-btn'
onClick={this.handleMoveLeft}
><Icon
id='chevron-left'
/></button >
<button
title={formatMessage(messages.moveRight)}
aria-label={formatMessage(messages.moveRight)}
className='text-btn column-header__setting-btn'
onClick={this.handleMoveRight}
><Icon
id='chevron-right'
/></button >
</div >
); );
} else if (multiColumn && this.props.onPin) { } else if (multiColumn && this.props.onPin) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; pinButton =
(<button
key='pin-button'
className='text-btn column-header__setting-btn'
onClick={this.handlePin}
><Icon
id='plus'
/> <FormattedMessage
id='column_header.pin'
defaultMessage='Pin'
/>
</button >);
} }
if (!pinned && (multiColumn || showBackButton)) { if (!pinned && (multiColumn || showBackButton)) {
backButton = ( backButton = (
<button onClick={this.handleBackClick} className='column-header__back-button'> <button
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> onClick={this.handleBackClick}
<FormattedMessage id='column_back_button.label' defaultMessage='Back' /> className='column-header__back-button'
</button> >
<Icon
id='chevron-left'
className='column-back-button__icon'
fixedWidth
/>
<FormattedMessage
id='column_back_button.label'
defaultMessage='Back'
/>
</button >
); );
} }
@ -143,7 +194,16 @@ class ColumnHeader extends React.PureComponent {
} }
if (children || (multiColumn && this.props.onPin)) { if (children || (multiColumn && this.props.onPin)) {
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>; collapseButton =
(<button
className={collapsibleButtonClassName}
title={formatMessage(collapsed ? messages.show : messages.hide)}
aria-label={formatMessage(collapsed ? messages.show : messages.hide)}
aria-pressed={collapsed ? 'false' : 'true'}
onClick={this.handleToggleClick}
>
<Icon id='sliders' />
</button >);
} }
const hasTitle = icon && title; const hasTitle = icon && title;
@ -153,9 +213,13 @@ class ColumnHeader extends React.PureComponent {
<h1 className={buttonClassName}> <h1 className={buttonClassName}>
{hasTitle && ( {hasTitle && (
<button onClick={this.handleTitleClick}> <button onClick={this.handleTitleClick}>
<Icon id={icon} fixedWidth className='column-header__icon' /> <Icon
id={icon}
fixedWidth
className='column-header__icon'
/>
{title} {title}
</button> </button >
)} )}
{!hasTitle && backButton} {!hasTitle && backButton}
@ -164,15 +228,19 @@ class ColumnHeader extends React.PureComponent {
{hasTitle && backButton} {hasTitle && backButton}
{extraButton} {extraButton}
{collapseButton} {collapseButton}
</div> </div >
</h1> </h1 >
<div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}> <div
className={collapsibleClassName}
tabIndex={collapsed ? -1 : null}
onTransitionEnd={this.handleTransitionEnd}
>
<div className='column-header__collapsible-inner'> <div className='column-header__collapsible-inner'>
{(!collapsed || animating) && collapsedContent} {(!collapsed || animating) && collapsedContent}
</div> </div >
</div> </div >
</div> </div >
); );
if (multiColumn || placeholder) { if (multiColumn || placeholder) {

View File

@ -6,12 +6,12 @@ import { autoPlayGif } from 'mastodon/initial_state';
export default class DisplayName extends React.PureComponent { export default class DisplayName extends React.PureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account : ImmutablePropTypes.map.isRequired,
others: ImmutablePropTypes.list, others : ImmutablePropTypes.list,
localDomain: PropTypes.string, localDomain: PropTypes.string,
}; };
_updateEmojis () { _updateEmojis() {
const node = this.node; const node = this.node;
if (!node || autoPlayGif) { if (!node || autoPlayGif) {
@ -32,33 +32,40 @@ export default class DisplayName extends React.PureComponent {
} }
} }
componentDidMount () { componentDidMount() {
this._updateEmojis(); this._updateEmojis();
} }
componentDidUpdate () { componentDidUpdate() {
this._updateEmojis(); this._updateEmojis();
} }
handleEmojiMouseEnter = ({ target }) => { handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original'); target.src = target.getAttribute('data-original');
} };
handleEmojiMouseLeave = ({ target }) => { handleEmojiMouseLeave = ({ target }) => {
target.src = target.getAttribute('data-static'); target.src = target.getAttribute('data-static');
} };
setRef = (c) => { setRef = (c) => {
this.node = c; this.node = c;
} };
render () { render() {
const { others, localDomain } = this.props; const { others, localDomain } = this.props;
let displayName, suffix, account; let displayName, suffix, account;
if (others && others.size > 1) { if (others && others.size > 1) {
displayName = others.take(2).map(a => <bdi key={a.get('id')}><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi>).reduce((prev, cur) => [prev, ', ', cur]); displayName = others.take(2).map(a =>
(<bdi key={a.get('id')}>
<strong
className='display-name__html'
dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
/>
</bdi >)).reduce((prev, cur) => [prev, ', ', cur]);
if (others.size - 2 > 0) { if (others.size - 2 > 0) {
suffix = `+${others.size - 2}`; suffix = `+${others.size - 2}`;
@ -76,14 +83,22 @@ export default class DisplayName extends React.PureComponent {
acct = `${acct}@${localDomain}`; acct = `${acct}@${localDomain}`;
} }
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>; displayName = <bdi ><strong
suffix = <span className='display-name__account'>@{acct}</span>; className='display-name__html'
dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }}
/>
</bdi >;
suffix = <span className='display-name__account'>@{acct}</span >;
} }
return ( return (
<span className='display-name' ref={this.setRef}> <span
className='display-name'
ref={this.setRef}
>
{displayName} {suffix} {displayName} {suffix}
</span> </span >
); );
} }

View File

@ -10,17 +10,17 @@ import StatusContent from './status_content';
import StatusActionBar from './status_action_bar'; import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list'; import AttachmentList from './attachment_list';
import Card from '../features/status/components/card'; import Card from '../features/status/components/card';
import { injectIntl, FormattedMessage } from 'react-intl'; import { FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; import { Audio, MediaGallery, Video } from '../features/ui/util/async-components';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import { displayMedia } from '../initial_state'; import { displayMedia } from '../initial_state';
// We use the component (and not the container) since we do not want // We use the component (and not the container) since we do not want
// to use the progress bar to show download progress // to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle'; import Bundle from '../features/ui/components/bundle';
import imageShowThread from '../../images/icon_reply.svg';
export const textForScreenReader = (intl, status, rebloggedByText = false) => { export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']); const displayName = status.getIn(['account', 'display_name']);
@ -59,33 +59,33 @@ class Status extends ImmutablePureComponent {
}; };
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map, status : ImmutablePropTypes.map,
account: ImmutablePropTypes.map, account : ImmutablePropTypes.map,
otherAccounts: ImmutablePropTypes.list, otherAccounts : ImmutablePropTypes.list,
onClick: PropTypes.func, onClick : PropTypes.func,
onReply: PropTypes.func, onReply : PropTypes.func,
onFavourite: PropTypes.func, onFavourite : PropTypes.func,
onReblog: PropTypes.func, onReblog : PropTypes.func,
onDelete: PropTypes.func, onDelete : PropTypes.func,
onDirect: PropTypes.func, onDirect : PropTypes.func,
onMention: PropTypes.func, onMention : PropTypes.func,
onPin: PropTypes.func, onPin : PropTypes.func,
onOpenMedia: PropTypes.func, onOpenMedia : PropTypes.func,
onOpenVideo: PropTypes.func, onOpenVideo : PropTypes.func,
onBlock: PropTypes.func, onBlock : PropTypes.func,
onEmbed: PropTypes.func, onEmbed : PropTypes.func,
onHeightChange: PropTypes.func, onHeightChange : PropTypes.func,
onToggleHidden: PropTypes.func, onToggleHidden : PropTypes.func,
muted: PropTypes.bool, muted : PropTypes.bool,
hidden: PropTypes.bool, hidden : PropTypes.bool,
unread: PropTypes.bool, unread : PropTypes.bool,
onMoveUp: PropTypes.func, onMoveUp : PropTypes.func,
onMoveDown: PropTypes.func, onMoveDown : PropTypes.func,
showThread: PropTypes.bool, showThread : PropTypes.bool,
getScrollPosition: PropTypes.func, getScrollPosition : PropTypes.func,
updateScrollBottom: PropTypes.func, updateScrollBottom: PropTypes.func,
cacheMediaWidth: PropTypes.func, cacheMediaWidth : PropTypes.func,
cachedMediaWidth: PropTypes.number, cachedMediaWidth : PropTypes.number,
}; };
// Avoid checking props that are functions (and whose equality will always // Avoid checking props that are functions (and whose equality will always
@ -99,15 +99,26 @@ class Status extends ImmutablePureComponent {
state = { state = {
showMedia: defaultMediaVisibility(this.props.status), showMedia: defaultMediaVisibility(this.props.status),
statusId: undefined, statusId : undefined,
}; };
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
return {
showMedia: defaultMediaVisibility(nextProps.status),
statusId : nextProps.status.get('id'),
};
} else {
return null;
}
}
// Track height changes we know about to compensate scrolling // Track height changes we know about to compensate scrolling
componentDidMount () { componentDidMount() {
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card'); this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
} }
getSnapshotBeforeUpdate () { getSnapshotBeforeUpdate() {
if (this.props.getScrollPosition) { if (this.props.getScrollPosition) {
return this.props.getScrollPosition(); return this.props.getScrollPosition();
} else { } else {
@ -115,20 +126,9 @@ class Status extends ImmutablePureComponent {
} }
} }
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
return {
showMedia: defaultMediaVisibility(nextProps.status),
statusId: nextProps.status.get('id'),
};
} else {
return null;
}
}
// Compensate height changes // Compensate height changes
componentDidUpdate (prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card'); const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
if (doShowCard && !this.didShowCard) { if (doShowCard && !this.didShowCard) {
this.didShowCard = true; this.didShowCard = true;
@ -154,7 +154,7 @@ class Status extends ImmutablePureComponent {
handleToggleMediaVisibility = () => { handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia }); this.setState({ showMedia: !this.state.showMedia });
} };
handleClick = () => { handleClick = () => {
if (this.props.onClick) { if (this.props.onClick) {
@ -168,7 +168,7 @@ class Status extends ImmutablePureComponent {
const { status } = this.props; const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`); this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
} };
handleExpandClick = (e) => { handleExpandClick = (e) => {
if (this.props.onClick) { if (this.props.onClick) {
@ -184,7 +184,7 @@ class Status extends ImmutablePureComponent {
const { status } = this.props; const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`); this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
} }
} };
handleAccountClick = (e) => { handleAccountClick = (e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
@ -192,27 +192,36 @@ class Status extends ImmutablePureComponent {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/accounts/${id}`); this.context.router.history.push(`/accounts/${id}`);
} }
} };
handleExpandedToggle = () => { handleExpandedToggle = () => {
this.props.onToggleHidden(this._properStatus()); this.props.onToggleHidden(this._properStatus());
}; };
renderLoadingMediaGallery () { renderLoadingMediaGallery() {
return <div className='media-gallery' style={{ height: '110px' }} />; return <div
className='media-gallery'
style={{ height: '110px' }}
/>;
} }
renderLoadingVideoPlayer () { renderLoadingVideoPlayer() {
return <div className='video-player' style={{ height: '110px' }} />; return <div
className='video-player'
style={{ height: '110px' }}
/>;
} }
renderLoadingAudioPlayer () { renderLoadingAudioPlayer() {
return <div className='audio-player' style={{ height: '110px' }} />; return <div
className='audio-player'
style={{ height: '110px' }}
/>;
} }
handleOpenVideo = (media, startTime) => { handleOpenVideo = (media, startTime) => {
this.props.onOpenVideo(media, startTime); this.props.onOpenVideo(media, startTime);
} };
handleHotkeyOpenMedia = e => { handleHotkeyOpenMedia = e => {
const { onOpenMedia, onOpenVideo } = this.props; const { onOpenMedia, onOpenVideo } = this.props;
@ -229,51 +238,51 @@ class Status extends ImmutablePureComponent {
onOpenMedia(status.get('media_attachments'), 0); onOpenMedia(status.get('media_attachments'), 0);
} }
} }
} };
handleHotkeyReply = e => { handleHotkeyReply = e => {
e.preventDefault(); e.preventDefault();
this.props.onReply(this._properStatus(), this.context.router.history); this.props.onReply(this._properStatus(), this.context.router.history);
} };
handleHotkeyFavourite = () => { handleHotkeyFavourite = () => {
this.props.onFavourite(this._properStatus()); this.props.onFavourite(this._properStatus());
} };
handleHotkeyBoost = e => { handleHotkeyBoost = e => {
this.props.onReblog(this._properStatus(), e); this.props.onReblog(this._properStatus(), e);
} };
handleHotkeyMention = e => { handleHotkeyMention = e => {
e.preventDefault(); e.preventDefault();
this.props.onMention(this._properStatus().get('account'), this.context.router.history); this.props.onMention(this._properStatus().get('account'), this.context.router.history);
} };
handleHotkeyOpen = () => { handleHotkeyOpen = () => {
this.context.router.history.push(`/statuses/${this._properStatus().get('id')}`); this.context.router.history.push(`/statuses/${this._properStatus().get('id')}`);
} };
handleHotkeyOpenProfile = () => { handleHotkeyOpenProfile = () => {
this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`); this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`);
} };
handleHotkeyMoveUp = e => { handleHotkeyMoveUp = e => {
this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured')); this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
} };
handleHotkeyMoveDown = e => { handleHotkeyMoveDown = e => {
this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured')); this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
} };
handleHotkeyToggleHidden = () => { handleHotkeyToggleHidden = () => {
this.props.onToggleHidden(this._properStatus()); this.props.onToggleHidden(this._properStatus());
} };
handleHotkeyToggleSensitive = () => { handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility(); this.handleToggleMediaVisibility();
} };
_properStatus () { _properStatus() {
const { status } = this.props; const { status } = this.props;
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
@ -285,9 +294,9 @@ class Status extends ImmutablePureComponent {
handleRef = c => { handleRef = c => {
this.node = c; this.node = c;
} };
render () { render() {
let media = null; let media = null;
let statusAvatar, prepend, rebloggedByText; let statusAvatar, prepend, rebloggedByText;
@ -300,66 +309,105 @@ class Status extends ImmutablePureComponent {
} }
const handlers = this.props.muted ? {} : { const handlers = this.props.muted ? {} : {
reply: this.handleHotkeyReply, reply : this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite, favourite : this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost, boost : this.handleHotkeyBoost,
mention: this.handleHotkeyMention, mention : this.handleHotkeyMention,
open: this.handleHotkeyOpen, open : this.handleHotkeyOpen,
openProfile: this.handleHotkeyOpenProfile, openProfile : this.handleHotkeyOpenProfile,
moveUp: this.handleHotkeyMoveUp, moveUp : this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown, moveDown : this.handleHotkeyMoveDown,
toggleHidden: this.handleHotkeyToggleHidden, toggleHidden : this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive, toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia, openMedia : this.handleHotkeyOpenMedia,
}; };
if (hidden) { if (hidden) {
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex='0'> <div
ref={this.handleRef}
className={classNames('status__wrapper', { focusable: !this.props.muted })}
tabIndex='0'
>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')} {status.get('content')}
</div> </div >
</HotKeys> </HotKeys >
); );
} }
if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) { if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) {
const minHandlers = this.props.muted ? {} : { const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp, moveUp : this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown, moveDown: this.handleHotkeyMoveDown,
}; };
return ( return (
<HotKeys handlers={minHandlers}> <HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}> <div
<FormattedMessage id='status.filtered' defaultMessage='Filtered' /> className='status__wrapper status__wrapper--filtered focusable'
</div> tabIndex='0'
</HotKeys> ref={this.handleRef}
>
<FormattedMessage
id='status.filtered'
defaultMessage='Filtered'
/>
</div >
</HotKeys >
); );
} }
if (featured) { if (featured) {
prepend = ( prepend = (
<div className='status__prepend'> <div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' className='status__prepend-icon' fixedWidth /></div> <div className='status__prepend-icon-wrapper'><Icon
<FormattedMessage id='status.pinned' defaultMessage='Pinned toot' /> id='thumb-tack'
</div> className='status__prepend-icon'
fixedWidth
/></div >
<FormattedMessage
id='status.pinned'
defaultMessage='Pinned toot'
/>
</div >
); );
} else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { } else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) }; const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
prepend = ( prepend = (
<div className='status__prepend'> <div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div> <div className='status__prepend-icon-wrapper'><Icon
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} /> id='retweet'
</div> className='status__prepend-icon'
fixedWidth
/>
</div >
<FormattedMessage
id='status.reblogged_by'
defaultMessage='{name} boosted'
values={{
name: <a
onClick={this.handleAccountClick}
data-id={status.getIn(['account', 'id'])}
href={status.getIn(['account', 'url'])}
className='status__display-name muted'
>
<bdi ><strong dangerouslySetInnerHTML={display_name_html} /></bdi >
</a >,
}}
/>
</div >
); );
rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: status.getIn(['account', 'acct']) }); rebloggedByText = intl.formatMessage({
id : 'status.reblogged_by',
defaultMessage: '{name} boosted',
}, { name: status.getIn(['account', 'acct']) });
account = status.get('account'); account = status.get('account');
status = status.get('reblog'); status = status.get('reblog');
} }
if (status.get('media_attachments').size > 0) { if (status.get('media_attachments').size > 0) {
@ -374,7 +422,10 @@ class Status extends ImmutablePureComponent {
const attachment = status.getIn(['media_attachments', 0]); const attachment = status.getIn(['media_attachments', 0]);
media = ( media = (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} > <Bundle
fetchComponent={Audio}
loading={this.renderLoadingAudioPlayer}
>
{Component => ( {Component => (
<Component <Component
src={attachment.get('url')} src={attachment.get('url')}
@ -384,13 +435,16 @@ class Status extends ImmutablePureComponent {
height={70} height={70}
/> />
)} )}
</Bundle> </Bundle >
); );
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]); const attachment = status.getIn(['media_attachments', 0]);
media = ( media = (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > <Bundle
fetchComponent={Video}
loading={this.renderLoadingVideoPlayer}
>
{Component => ( {Component => (
<Component <Component
preview={attachment.get('preview_url')} preview={attachment.get('preview_url')}
@ -407,11 +461,14 @@ class Status extends ImmutablePureComponent {
onToggleVisibility={this.handleToggleMediaVisibility} onToggleVisibility={this.handleToggleMediaVisibility}
/> />
)} )}
</Bundle> </Bundle >
); );
} else { } else {
media = ( media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}> <Bundle
fetchComponent={MediaGallery}
loading={this.renderLoadingMediaGallery}
>
{Component => ( {Component => (
<Component <Component
media={status.get('media_attachments')} media={status.get('media_attachments')}
@ -424,7 +481,7 @@ class Status extends ImmutablePureComponent {
onToggleVisibility={this.handleToggleMediaVisibility} onToggleVisibility={this.handleToggleMediaVisibility}
/> />
)} )}
</Bundle> </Bundle >
); );
} }
} else if (status.get('spoiler_text').length === 0 && status.get('card')) { } else if (status.get('spoiler_text').length === 0 && status.get('card')) {
@ -440,46 +497,111 @@ class Status extends ImmutablePureComponent {
} }
if (otherAccounts && otherAccounts.size > 0) { if (otherAccounts && otherAccounts.size > 0) {
statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} />; statusAvatar = <AvatarComposite
accounts={otherAccounts}
size={48}
/>;
} else if (account === undefined || account === null) { } else if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={48} />; statusAvatar = <Avatar
account={status.get('account')}
size={48}
/>;
} else { } else {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />; statusAvatar = <AvatarOverlay
account={status.get('account')}
friend={account}
/>;
} }
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}> <div
className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, {
'status__wrapper-reply': !!status.get('in_reply_to_id'),
read : unread === false,
focusable : !this.props.muted,
})}
tabIndex={this.props.muted ? null : 0}
data-featured={featured ? 'true' : null}
aria-label={textForScreenReader(intl, status, rebloggedByText)}
ref={this.handleRef}
>
{prepend} {prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}> <div
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' /> className={classNames('status', `status-${status.get('visibility')}`, {
'status-reply': !!status.get('in_reply_to_id'),
muted : this.props.muted,
read : unread === false,
})}
data-id={status.get('id')}
>
<div
className='status__expand'
onClick={this.handleExpandClick}
role='presentation'
/>
<div className='status__info'> <div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a> <a
href={status.get('url')}
className='status__relative-time'
target='_blank'
rel='noopener noreferrer'
><RelativeTimestamp timestamp={status.get('created_at')} /></a >
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'> <a
onClick={this.handleAccountClick}
data-id={status.getIn(['account', 'id'])}
href={status.getIn(['account', 'url'])}
title={status.getIn(['account', 'acct'])}
className='status__display-name'
target='_blank'
rel='noopener noreferrer'
>
<div className='status__avatar'> <div className='status__avatar'>
{statusAvatar} {statusAvatar}
</div> </div >
<DisplayName account={status.get('account')} others={otherAccounts} /> <DisplayName
</a> account={status.get('account')}
</div> others={otherAccounts}
/>
</a >
</div >
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable /> <StatusContent
status={status}
onClick={this.handleClick}
expanded={!status.get('hidden')}
onExpandedToggle={this.handleExpandedToggle}
collapsable
/>
{media} {media}
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && ( {showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
<button className='status__content__read-more-button' onClick={this.handleClick}> <button
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' /> className='status__content__read-more-button'
</button> onClick={this.handleClick}
>
<FormattedMessage
id='status.show_thread'
defaultMessage='Show thread'
/>
<img
src={imageShowThread}
alt='=> '
/>
</button >
)} )}
<StatusActionBar status={status} account={account} {...other} /> <StatusActionBar
</div> status={status}
</div> account={account} {...other} />
</HotKeys> </div >
</div >
</HotKeys >
); );
} }

View File

@ -5,36 +5,36 @@ import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { me, isStaff } from '../../../initial_state'; import { isStaff, me } from '../../../initial_state';
const messages = defineMessages({ const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' }, delete : { id: 'status.delete', defaultMessage: 'Delete' },
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, redraft : { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, direct : { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, mention : { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' }, reply : { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, reblog : { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' }, reblog_private : { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, cannot_reblog : { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, favourite : { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, bookmark : { id: 'status.bookmark', defaultMessage: 'Bookmark' },
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' }, mute : { id: 'status.mute', defaultMessage: 'Mute @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, muteConversation : { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, unmuteConversation : { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
block: { id: 'status.block', defaultMessage: 'Block @{name}' }, block : { id: 'status.block', defaultMessage: 'Block @{name}' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' }, report : { id: 'status.report', defaultMessage: 'Report @{name}' },
share: { id: 'status.share', defaultMessage: 'Share' }, share : { id: 'status.share', defaultMessage: 'Share' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, pin : { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, unpin : { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' }, embed : { id: 'status.embed', defaultMessage: 'Embed' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, 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' }, admin_status : { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, copy : { id: 'status.copy', defaultMessage: 'Copy link to status' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, blockDomain : { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, unblockDomain : { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unmute : { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unblock : { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
}); });
const mapStateToProps = (state, { status }) => ({ const mapStateToProps = (state, { status }) => ({
@ -50,59 +50,59 @@ class ActionBar extends React.PureComponent {
}; };
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map.isRequired, status : ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map, relationship : ImmutablePropTypes.map,
onReply: PropTypes.func.isRequired, onReply : PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired, onReblog : PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired, onFavourite : PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired, onBookmark : PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete : PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired, onDirect : PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired, onMention : PropTypes.func.isRequired,
onMute: PropTypes.func, onMute : PropTypes.func,
onUnmute: PropTypes.func, onUnmute : PropTypes.func,
onBlock: PropTypes.func, onBlock : PropTypes.func,
onUnblock: PropTypes.func, onUnblock : PropTypes.func,
onBlockDomain: PropTypes.func, onBlockDomain : PropTypes.func,
onUnblockDomain: PropTypes.func, onUnblockDomain : PropTypes.func,
onMuteConversation: PropTypes.func, onMuteConversation: PropTypes.func,
onReport: PropTypes.func, onReport : PropTypes.func,
onPin: PropTypes.func, onPin : PropTypes.func,
onEmbed: PropTypes.func, onEmbed : PropTypes.func,
intl: PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
}; };
handleReplyClick = () => { handleReplyClick = () => {
this.props.onReply(this.props.status); this.props.onReply(this.props.status);
} };
handleReblogClick = (e) => { handleReblogClick = (e) => {
this.props.onReblog(this.props.status, e); this.props.onReblog(this.props.status, e);
} };
handleFavouriteClick = () => { handleFavouriteClick = () => {
this.props.onFavourite(this.props.status); this.props.onFavourite(this.props.status);
} };
handleBookmarkClick = (e) => { handleBookmarkClick = (e) => {
this.props.onBookmark(this.props.status, e); this.props.onBookmark(this.props.status, e);
} };
handleDeleteClick = () => { handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history); this.props.onDelete(this.props.status, this.context.router.history);
} };
handleRedraftClick = () => { handleRedraftClick = () => {
this.props.onDelete(this.props.status, this.context.router.history, true); this.props.onDelete(this.props.status, this.context.router.history, true);
} };
handleDirectClick = () => { handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.context.router.history); this.props.onDirect(this.props.status.get('account'), this.context.router.history);
} };
handleMentionClick = () => { handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history); this.props.onMention(this.props.status.get('account'), this.context.router.history);
} };
handleMuteClick = () => { handleMuteClick = () => {
const { status, relationship, onMute, onUnmute } = this.props; const { status, relationship, onMute, onUnmute } = this.props;
@ -113,7 +113,7 @@ class ActionBar extends React.PureComponent {
} else { } else {
onMute(account); onMute(account);
} }
} };
handleBlockClick = () => { handleBlockClick = () => {
const { status, relationship, onBlock, onUnblock } = this.props; const { status, relationship, onBlock, onUnblock } = this.props;
@ -124,50 +124,50 @@ class ActionBar extends React.PureComponent {
} else { } else {
onBlock(status); onBlock(status);
} }
} };
handleBlockDomain = () => { handleBlockDomain = () => {
const { status, onBlockDomain } = this.props; const { status, onBlockDomain } = this.props;
const account = status.get('account'); const account = status.get('account');
onBlockDomain(account.get('acct').split('@')[1]); onBlockDomain(account.get('acct').split('@')[1]);
} };
handleUnblockDomain = () => { handleUnblockDomain = () => {
const { status, onUnblockDomain } = this.props; const { status, onUnblockDomain } = this.props;
const account = status.get('account'); const account = status.get('account');
onUnblockDomain(account.get('acct').split('@')[1]); onUnblockDomain(account.get('acct').split('@')[1]);
} };
handleConversationMuteClick = () => { handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status); this.props.onMuteConversation(this.props.status);
} };
handleReport = () => { handleReport = () => {
this.props.onReport(this.props.status); this.props.onReport(this.props.status);
} };
handlePinClick = () => { handlePinClick = () => {
this.props.onPin(this.props.status); this.props.onPin(this.props.status);
} };
handleShare = () => { handleShare = () => {
navigator.share({ navigator.share({
text: this.props.status.get('search_index'), text: this.props.status.get('search_index'),
url: this.props.status.get('url'), url : this.props.status.get('url'),
}); });
} };
handleEmbed = () => { handleEmbed = () => {
this.props.onEmbed(this.props.status); this.props.onEmbed(this.props.status);
} };
handleCopy = () => { handleCopy = () => {
const url = this.props.status.get('url'); const url = this.props.status.get('url');
const textarea = document.createElement('textarea'); const textarea = document.createElement('textarea');
textarea.textContent = url; textarea.textContent = url;
textarea.style.position = 'fixed'; textarea.style.position = 'fixed';
document.body.appendChild(textarea); document.body.appendChild(textarea);
@ -180,14 +180,14 @@ class ActionBar extends React.PureComponent {
} finally { } finally {
document.body.removeChild(textarea); document.body.removeChild(textarea);
} }
} };
render () { render() {
const { status, relationship, intl } = this.props; const { status, relationship, intl } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const mutingConversation = status.get('muted'); const mutingConversation = status.get('muted');
const account = status.get('account'); const account = status.get('account');
let menu = []; let menu = [];
@ -199,36 +199,66 @@ class ActionBar extends React.PureComponent {
if (me === status.getIn(['account', 'id'])) { if (me === status.getIn(['account', 'id'])) {
if (publicStatus) { if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); menu.push({
text : intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin),
action: this.handlePinClick,
});
} else { } else {
if (status.get('visibility') === 'private') { if (status.get('visibility') === 'private') {
menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick }); menu.push({
text : intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private),
action: this.handleReblogClick,
});
} }
} }
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); menu.push({
text : intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation),
action: this.handleConversationMuteClick,
});
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else { } else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); text : intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }),
action: this.handleMentionClick,
});
menu.push({
text : intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }),
action: this.handleDirectClick,
});
menu.push(null); menu.push(null);
if (relationship && relationship.get('muting')) { if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); menu.push({
text : intl.formatMessage(messages.unmute, { name: account.get('username') }),
action: this.handleMuteClick,
});
} else { } else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick }); menu.push({
text : intl.formatMessage(messages.mute, { name: account.get('username') }),
action: this.handleMuteClick,
});
} }
if (relationship && relationship.get('blocking')) { if (relationship && relationship.get('blocking')) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick }); menu.push({
text : intl.formatMessage(messages.unblock, { name: account.get('username') }),
action: this.handleBlockClick,
});
} else { } else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick }); menu.push({
text : intl.formatMessage(messages.block, { name: account.get('username') }),
action: this.handleBlockClick,
});
} }
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); menu.push({
text : intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }),
action: this.handleReport,
});
if (account.get('acct') !== account.get('username')) { if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1]; const domain = account.get('acct').split('@')[1];
@ -244,13 +274,23 @@ class ActionBar extends React.PureComponent {
if (isStaff) { if (isStaff) {
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); menu.push({
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }),
href: `/admin/accounts/${status.getIn(['account', 'id'])}`,
});
menu.push({
text: intl.formatMessage(messages.admin_status),
href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}`,
});
} }
} }
const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && ( const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div> <div className='detailed-status__button'><IconButton
title={intl.formatMessage(messages.share)}
icon='share-alt'
onClick={this.handleShare}
/></div >
); );
let replyIcon; let replyIcon;
@ -268,16 +308,48 @@ class ActionBar extends React.PureComponent {
return ( return (
<div className='detailed-status__action-bar'> <div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div> <div className='detailed-status__button'><IconButton
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> title={intl.formatMessage(messages.reply)}
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div> icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon}
onClick={this.handleReplyClick}
/></div >
<div className='detailed-status__button'><IconButton
disabled={reblog_disabled}
active={status.get('reblogged')}
title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)}
icon={reblogIcon}
onClick={this.handleReblogClick}
/></div >
<div className='detailed-status__button'>
<IconButton
className='star-icon'
animate
active={status.get('favourited')}
title={intl.formatMessage(messages.favourite)}
icon='star'
onClick={this.handleFavouriteClick}
/></div >
{shareButton} {shareButton}
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> <div className='detailed-status__button'>
<IconButton
className='bookmark-icon'
active={status.get('bookmarked')}
title={intl.formatMessage(messages.bookmark)}
icon='bookmark'
onClick={this.handleBookmarkClick}
/></div >
<div className='detailed-status__action-bar-dropdown'> <div className='detailed-status__action-bar-dropdown'>
<DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title='More' /> <DropdownMenuContainer
</div> size={18}
</div> icon='ellipsis-h'
status={status}
items={menu}
direction='left'
title='More'
/>
</div >
</div >
); );
} }

View File

@ -5,41 +5,24 @@ import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { fetchStatus } from '../../actions/statuses'; import { deleteStatus, fetchStatus, hideStatus, muteStatus, revealStatus, unmuteStatus } from '../../actions/statuses';
import MissingIndicator from '../../components/missing_indicator'; import MissingIndicator from '../../components/missing_indicator';
import DetailedStatus from './components/detailed_status'; import DetailedStatus from './components/detailed_status';
import ActionBar from './components/action_bar'; import ActionBar from './components/action_bar';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import { import {
favourite,
unfavourite,
bookmark, bookmark,
unbookmark, favourite,
reblog,
unreblog,
pin, pin,
reblog,
unbookmark,
unfavourite,
unpin, unpin,
unreblog,
} from '../../actions/interactions'; } from '../../actions/interactions';
import { import { directCompose, mentionCompose, replyCompose } from '../../actions/compose';
replyCompose, import { unblockAccount, unmuteAccount } from '../../actions/accounts';
mentionCompose, import { blockDomain, unblockDomain } from '../../actions/domain_blocks';
directCompose,
} from '../../actions/compose';
import {
muteStatus,
unmuteStatus,
deleteStatus,
hideStatus,
revealStatus,
} from '../../actions/statuses';
import {
unblockAccount,
unmuteAccount,
} from '../../actions/accounts';
import {
blockDomain,
unblockDomain,
} from '../../actions/domain_blocks';
import { initMuteModal } from '../../actions/mutes'; import { initMuteModal } from '../../actions/mutes';
import { initBlockModal } from '../../actions/blocks'; import { initBlockModal } from '../../actions/blocks';
import { initReport } from '../../actions/reports'; import { initReport } from '../../actions/reports';
@ -49,24 +32,33 @@ import ColumnBackButton from '../../components/column_back_button';
import ColumnHeader from '../../components/column_header'; import ColumnHeader from '../../components/column_header';
import StatusContainer from '../../containers/status_container'; import StatusContainer from '../../containers/status_container';
import { openModal } from '../../actions/modal'; import { openModal } from '../../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal } from '../../initial_state'; import { boostModal, deleteModal } from '../../initial_state';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; import { defaultMediaVisibility, textForScreenReader } from '../../components/status';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteConfirm : { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, deleteMessage : {
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, id : 'confirmations.delete.message',
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, defaultMessage: 'Are you sure you want to delete this status?',
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' }, },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' }, redraftConfirm : { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, redraftMessage : {
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, id : 'confirmations.redraft.message',
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.',
},
revealAll : { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll : { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
detailedStatus : { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
replyConfirm : { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage : {
id : 'confirmations.reply.message',
defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?',
},
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
}); });
@ -99,7 +91,7 @@ const makeMapStateToProps = () => {
const ids = [statusId]; const ids = [statusId];
while (ids.length > 0) { while (ids.length > 0) {
let id = ids.shift(); let id = ids.shift();
const replies = contextReplies.get(id); const replies = contextReplies.get(id);
if (statusId !== id) { if (statusId !== id) {
@ -116,7 +108,8 @@ const makeMapStateToProps = () => {
let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account')); let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account'));
if (insertAt !== -1) { if (insertAt !== -1) {
descendantsIds.forEach((id, idx) => { descendantsIds.forEach((id, idx) => {
if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) { let account = statuses.get(id).get('account');
if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === account) {
descendantsIds.splice(idx, 1); descendantsIds.splice(idx, 1);
descendantsIds.splice(insertAt, 0, id); descendantsIds.splice(insertAt, 0, id);
insertAt += 1; insertAt += 1;
@ -142,7 +135,7 @@ const makeMapStateToProps = () => {
ancestorsIds, ancestorsIds,
descendantsIds, descendantsIds,
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
domain: state.getIn(['meta', 'domain']), domain : state.getIn(['meta', 'domain']),
}; };
}; };
@ -158,45 +151,48 @@ class Status extends ImmutablePureComponent {
}; };
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params : PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch : PropTypes.func.isRequired,
status: ImmutablePropTypes.map, status : ImmutablePropTypes.map,
ancestorsIds: ImmutablePropTypes.list, ancestorsIds : ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list, descendantsIds : ImmutablePropTypes.list,
intl: PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool, askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn : PropTypes.bool,
domain: PropTypes.string.isRequired, domain : PropTypes.string.isRequired,
}; };
state = { state = {
fullscreen: false, fullscreen : false,
showMedia: defaultMediaVisibility(this.props.status), showMedia : defaultMediaVisibility(this.props.status),
loadedStatusId: undefined, loadedStatusId: undefined,
}; };
componentWillMount () { componentWillMount() {
this.props.dispatch(fetchStatus(this.props.params.statusId)); this.props.dispatch(fetchStatus(this.props.params.statusId));
} }
componentDidMount () { componentDidMount() {
attachFullscreenListener(this.onFullScreenChange); attachFullscreenListener(this.onFullScreenChange);
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps(nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this._scrolledIntoView = false; this._scrolledIntoView = false;
this.props.dispatch(fetchStatus(nextProps.params.statusId)); this.props.dispatch(fetchStatus(nextProps.params.statusId));
} }
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) { if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') }); this.setState({
showMedia : defaultMediaVisibility(nextProps.status),
loadedStatusId: nextProps.status.get('id'),
});
} }
} }
handleToggleMediaVisibility = () => { handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia }); this.setState({ showMedia: !this.state.showMedia });
} };
handleFavouriteClick = (status) => { handleFavouriteClick = (status) => {
if (status.get('favourited')) { if (status.get('favourited')) {
@ -204,7 +200,7 @@ class Status extends ImmutablePureComponent {
} else { } else {
this.props.dispatch(favourite(status)); this.props.dispatch(favourite(status));
} }
} };
handlePin = (status) => { handlePin = (status) => {
if (status.get('pinned')) { if (status.get('pinned')) {
@ -212,24 +208,24 @@ class Status extends ImmutablePureComponent {
} else { } else {
this.props.dispatch(pin(status)); this.props.dispatch(pin(status));
} }
} };
handleReplyClick = (status) => { handleReplyClick = (status) => {
let { askReplyConfirmation, dispatch, intl } = this.props; let { askReplyConfirmation, dispatch, intl } = this.props;
if (askReplyConfirmation) { if (askReplyConfirmation) {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.replyMessage), message : intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm), confirm : intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, this.context.router.history)), onConfirm: () => dispatch(replyCompose(status, this.context.router.history)),
})); }));
} else { } else {
dispatch(replyCompose(status, this.context.router.history)); dispatch(replyCompose(status, this.context.router.history));
} }
} };
handleModalReblog = (status) => { handleModalReblog = (status) => {
this.props.dispatch(reblog(status)); this.props.dispatch(reblog(status));
} };
handleReblogClick = (status, e) => { handleReblogClick = (status, e) => {
if (status.get('reblogged')) { if (status.get('reblogged')) {
@ -241,7 +237,7 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog })); this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
} }
} }
} };
handleBookmarkClick = (status) => { handleBookmarkClick = (status) => {
if (status.get('bookmarked')) { if (status.get('bookmarked')) {
@ -249,7 +245,7 @@ class Status extends ImmutablePureComponent {
} else { } else {
this.props.dispatch(bookmark(status)); this.props.dispatch(bookmark(status));
} }
} };
handleDeleteClick = (status, history, withRedraft = false) => { handleDeleteClick = (status, history, withRedraft = false) => {
const { dispatch, intl } = this.props; const { dispatch, intl } = this.props;
@ -258,28 +254,28 @@ class Status extends ImmutablePureComponent {
dispatch(deleteStatus(status.get('id'), history, withRedraft)); dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), message : intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), confirm : intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
})); }));
} }
} };
handleDirectClick = (account, router) => { handleDirectClick = (account, router) => {
this.props.dispatch(directCompose(account, router)); this.props.dispatch(directCompose(account, router));
} };
handleMentionClick = (account, router) => { handleMentionClick = (account, router) => {
this.props.dispatch(mentionCompose(account, router)); this.props.dispatch(mentionCompose(account, router));
} };
handleOpenMedia = (media, index) => { handleOpenMedia = (media, index) => {
this.props.dispatch(openModal('MEDIA', { media, index })); this.props.dispatch(openModal('MEDIA', { media, index }));
} };
handleOpenVideo = (media, time) => { handleOpenVideo = (media, time) => {
this.props.dispatch(openModal('VIDEO', { media, time })); this.props.dispatch(openModal('VIDEO', { media, time }));
} };
handleHotkeyOpenMedia = e => { handleHotkeyOpenMedia = e => {
const status = this._properStatus(); const status = this._properStatus();
@ -295,11 +291,11 @@ class Status extends ImmutablePureComponent {
this.handleOpenMedia(status.get('media_attachments'), 0); this.handleOpenMedia(status.get('media_attachments'), 0);
} }
} }
} };
handleMuteClick = (account) => { handleMuteClick = (account) => {
this.props.dispatch(initMuteModal(account)); this.props.dispatch(initMuteModal(account));
} };
handleConversationMuteClick = (status) => { handleConversationMuteClick = (status) => {
if (status.get('muted')) { if (status.get('muted')) {
@ -307,7 +303,7 @@ class Status extends ImmutablePureComponent {
} else { } else {
this.props.dispatch(muteStatus(status.get('id'))); this.props.dispatch(muteStatus(status.get('id')));
} }
} };
handleToggleHidden = (status) => { handleToggleHidden = (status) => {
if (status.get('hidden')) { if (status.get('hidden')) {
@ -315,7 +311,7 @@ class Status extends ImmutablePureComponent {
} else { } else {
this.props.dispatch(hideStatus(status.get('id'))); this.props.dispatch(hideStatus(status.get('id')));
} }
} };
handleToggleAll = () => { handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
@ -326,80 +322,83 @@ class Status extends ImmutablePureComponent {
} else { } else {
this.props.dispatch(hideStatus(statusIds)); this.props.dispatch(hideStatus(statusIds));
} }
} };
handleBlockClick = (status) => { handleBlockClick = (status) => {
const { dispatch } = this.props; const { dispatch } = this.props;
const account = status.get('account'); const account = status.get('account');
dispatch(initBlockModal(account)); dispatch(initBlockModal(account));
} };
handleReport = (status) => { handleReport = (status) => {
this.props.dispatch(initReport(status.get('account'), status)); this.props.dispatch(initReport(status.get('account'), status));
} };
handleEmbed = (status) => { handleEmbed = (status) => {
this.props.dispatch(openModal('EMBED', { url: status.get('url') })); this.props.dispatch(openModal('EMBED', { url: status.get('url') }));
} };
handleUnmuteClick = account => { handleUnmuteClick = account => {
this.props.dispatch(unmuteAccount(account.get('id'))); this.props.dispatch(unmuteAccount(account.get('id')));
} };
handleUnblockClick = account => { handleUnblockClick = account => {
this.props.dispatch(unblockAccount(account.get('id'))); this.props.dispatch(unblockAccount(account.get('id')));
} };
handleBlockDomainClick = domain => { handleBlockDomainClick = domain => {
this.props.dispatch(openModal('CONFIRM', { this.props.dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />, message : <FormattedMessage
confirm: this.props.intl.formatMessage(messages.blockDomainConfirm), id='confirmations.domain_block.message'
defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.'
values={{ domain: <strong >{domain}</strong > }}
/>,
confirm : this.props.intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => this.props.dispatch(blockDomain(domain)), onConfirm: () => this.props.dispatch(blockDomain(domain)),
})); }));
} };
handleUnblockDomainClick = domain => { handleUnblockDomainClick = domain => {
this.props.dispatch(unblockDomain(domain)); this.props.dispatch(unblockDomain(domain));
} };
handleHotkeyMoveUp = () => { handleHotkeyMoveUp = () => {
this.handleMoveUp(this.props.status.get('id')); this.handleMoveUp(this.props.status.get('id'));
} };
handleHotkeyMoveDown = () => { handleHotkeyMoveDown = () => {
this.handleMoveDown(this.props.status.get('id')); this.handleMoveDown(this.props.status.get('id'));
} };
handleHotkeyReply = e => { handleHotkeyReply = e => {
e.preventDefault(); e.preventDefault();
this.handleReplyClick(this.props.status); this.handleReplyClick(this.props.status);
} };
handleHotkeyFavourite = () => { handleHotkeyFavourite = () => {
this.handleFavouriteClick(this.props.status); this.handleFavouriteClick(this.props.status);
} };
handleHotkeyBoost = () => { handleHotkeyBoost = () => {
this.handleReblogClick(this.props.status); this.handleReblogClick(this.props.status);
} };
handleHotkeyMention = e => { handleHotkeyMention = e => {
e.preventDefault(); e.preventDefault();
this.handleMentionClick(this.props.status.get('account')); this.handleMentionClick(this.props.status.get('account'));
} };
handleHotkeyOpenProfile = () => { handleHotkeyOpenProfile = () => {
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
} };
handleHotkeyToggleHidden = () => { handleHotkeyToggleHidden = () => {
this.handleToggleHidden(this.props.status); this.handleToggleHidden(this.props.status);
} };
handleHotkeyToggleSensitive = () => { handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility(); this.handleToggleMediaVisibility();
} };
handleMoveUp = id => { handleMoveUp = id => {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
@ -416,7 +415,7 @@ class Status extends ImmutablePureComponent {
this._selectChild(index - 1, true); this._selectChild(index - 1, true);
} }
} }
} };
handleMoveDown = id => { handleMoveDown = id => {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
@ -433,9 +432,9 @@ class Status extends ImmutablePureComponent {
this._selectChild(index + 1, false); this._selectChild(index + 1, false);
} }
} }
} };
_selectChild (index, align_top) { _selectChild(index, align_top) {
const container = this.node; const container = this.node;
const element = container.querySelectorAll('.focusable')[index]; const element = container.querySelectorAll('.focusable')[index];
@ -449,7 +448,7 @@ class Status extends ImmutablePureComponent {
} }
} }
renderChildren (list) { renderChildren(list) {
return list.map(id => ( return list.map(id => (
<StatusContainer <StatusContainer
key={id} key={id}
@ -463,9 +462,9 @@ class Status extends ImmutablePureComponent {
setRef = c => { setRef = c => {
this.node = c; this.node = c;
} };
componentDidUpdate () { componentDidUpdate() {
if (this._scrolledIntoView) { if (this._scrolledIntoView) {
return; return;
} }
@ -482,65 +481,84 @@ class Status extends ImmutablePureComponent {
} }
} }
componentWillUnmount () { componentWillUnmount() {
detachFullscreenListener(this.onFullScreenChange); detachFullscreenListener(this.onFullScreenChange);
} }
onFullScreenChange = () => { onFullScreenChange = () => {
this.setState({ fullscreen: isFullscreen() }); this.setState({ fullscreen: isFullscreen() });
} };
render () { render() {
let ancestors, descendants; let ancestors, descendants;
const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn } = this.props; const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn } = this.props;
const { fullscreen } = this.state; const { fullscreen } = this.state;
if (status === null) { if (status === null) {
return ( return (
<Column> <Column >
<ColumnBackButton multiColumn={multiColumn} /> <ColumnBackButton multiColumn={multiColumn} />
<MissingIndicator /> <MissingIndicator />
</Column> </Column >
); );
} }
if (ancestorsIds && ancestorsIds.size > 0) { if (ancestorsIds && ancestorsIds.size > 0) {
ancestors = <div>{this.renderChildren(ancestorsIds)}</div>; ancestors = <div >{this.renderChildren(ancestorsIds)}</div >;
} }
if (descendantsIds && descendantsIds.size > 0) { if (descendantsIds && descendantsIds.size > 0) {
descendants = <div>{this.renderChildren(descendantsIds)}</div>; descendants = <div >{this.renderChildren(descendantsIds)}</div >;
} }
const handlers = { const handlers = {
moveUp: this.handleHotkeyMoveUp, moveUp : this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown, moveDown : this.handleHotkeyMoveDown,
reply: this.handleHotkeyReply, reply : this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite, favourite : this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost, boost : this.handleHotkeyBoost,
mention: this.handleHotkeyMention, mention : this.handleHotkeyMention,
openProfile: this.handleHotkeyOpenProfile, openProfile : this.handleHotkeyOpenProfile,
toggleHidden: this.handleHotkeyToggleHidden, toggleHidden : this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive, toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia, openMedia : this.handleHotkeyOpenMedia,
}; };
return ( return (
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.detailedStatus)}> <Column
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.detailedStatus)}
>
<ColumnHeader <ColumnHeader
showBackButton showBackButton
multiColumn={multiColumn} multiColumn={multiColumn}
extraButton={( extraButton={(
<button className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={status.get('hidden') ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button> <button
className='column-header__button'
title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)}
aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)}
onClick={this.handleToggleAll}
aria-pressed={status.get('hidden') ? 'false' : 'true'}
><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button >
)} )}
/> />
<ScrollContainer scrollKey='thread' shouldUpdateScroll={shouldUpdateScroll}> <ScrollContainer
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}> scrollKey='thread'
shouldUpdateScroll={shouldUpdateScroll}
>
<div
className={classNames('scrollable', { fullscreen })}
ref={this.setRef}
>
{ancestors} {ancestors}
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className={classNames('focusable', 'detailed-status__wrapper')} tabIndex='0' aria-label={textForScreenReader(intl, status, false)}> <div
className={classNames('focusable', 'detailed-status__wrapper')}
tabIndex='0'
aria-label={textForScreenReader(intl, status, false)}
>
<DetailedStatus <DetailedStatus
status={status} status={status}
onOpenVideo={this.handleOpenVideo} onOpenVideo={this.handleOpenVideo}
@ -571,13 +589,13 @@ class Status extends ImmutablePureComponent {
onPin={this.handlePin} onPin={this.handlePin}
onEmbed={this.handleEmbed} onEmbed={this.handleEmbed}
/> />
</div> </div >
</HotKeys> </HotKeys >
{descendants} {descendants}
</div> </div >
</ScrollContainer> </ScrollContainer >
</Column> </Column >
); );
} }

View File

@ -46,75 +46,123 @@ class LinkFooter extends React.PureComponent {
return ( return (
<div className='getting-started__footer'> <div className='getting-started__footer'>
<ul> <ul >
<li> <li >
<a href="https://liberapay.com/cipherbliss">Supportez Cipherbliss</a> <a href='https://liberapay.com/cipherbliss'>Supportez Cipherbliss</a >
</li> </li >
<li> <li >
<a href="/@tykayn"> <a href='https://mastodon.cipherbliss.com/@tykayn'>
<i className="fa fa-envelope"></i> <i className='fa fa-paper-plane' />
contactez Cipherbliss</a> contactez nous</a >
</li> </li >
<li> <li >
<a href='/admin/tags?pending_review=1'> <a href='/admin/tags?pending_review=1'>
<i className="fa fa-fire"></i> <i className='fa fa-fire' />
Trending hashtags</a> Trending hashtags</a >
<hr/> <hr />
</li> </li >
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage {invitesEnabled && <li ><a
href='/invites'
target='_blank'
><FormattedMessage
id='getting_started.invite' id='getting_started.invite'
defaultMessage='Invite people' defaultMessage='Invite people'
/></a> · /> ·</a >
</li>} </li >}
{withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage {withHotkeys && <li ><Link to='/keyboard-shortcuts'>
id='navigation_bar.keyboard_shortcuts' <FormattedMessage
defaultMessage='Hotkeys' id='navigation_bar.keyboard_shortcuts'
/></Link> · defaultMessage='Hotkeys'
</li>} /> ·
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security'/></a> · </Link >
</li> </li >}
<li><a href='/about/more' target='_blank'><FormattedMessage <li >
id='navigation_bar.info' <a href='/auth/edit'>
defaultMessage='About this server' <FormattedMessage
/></a> · id='getting_started.security'
</li> defaultMessage='Security'
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage /> ·
id='navigation_bar.apps' </a >
defaultMessage='Mobile apps' </li >
/></a> · <li >
</li> <a
<li><a href='/terms' target='_blank'><FormattedMessage href='/about/more'
id='getting_started.terms' target='_blank'
defaultMessage='Terms of service' ><FormattedMessage
/></a> · id='navigation_bar.info'
</li> defaultMessage='About this server'
<li><a href='/settings/applications' target='_blank'><FormattedMessage /> ·
id='getting_started.developers' </a >
defaultMessage='Developers' </li >
/></a> · <li >
</li> <a
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage href='https://joinmastodon.org/apps'
id='getting_started.documentation' defaultMessage='Documentation' target='_blank'
/></a> · ><FormattedMessage
</li> id='navigation_bar.apps'
<li><a href='/auth/sign_out' onClick={this.handleLogoutClick}><FormattedMessage defaultMessage='Mobile apps'
id='navigation_bar.logout' /> ·
defaultMessage='Logout' </a >
/></a> </li >
</li> <li >
</ul> <a
href='/terms'
target='_blank'
>
<FormattedMessage
id='getting_started.terms'
defaultMessage='Terms of service'
/> ·</a >
</li >
<li >
<a
href='/settings/applications'
target='_blank'
><FormattedMessage
id='getting_started.developers'
defaultMessage='Developers'
/> ·
</a >
</li >
<li >
<a
href='https://docs.joinmastodon.org'
target='_blank'
>
<FormattedMessage
id='getting_started.documentation'
defaultMessage='Documentation'
/>
</a > ·
</li >
<li >
<a
href='/auth/sign_out'
onClick={this.handleLogoutClick}
>
<FormattedMessage
id='navigation_bar.logout'
defaultMessage='Logout'
/>
</a >
</li >
</ul >
<p> <p >
<FormattedMessage <FormattedMessage
id='getting_started.open_source_notice' id='getting_started.open_source_notice'
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.' defaultMessage='Mastodon is open source software. You can contribute or report issues on the forge at {forge}.'
values={{ values={{
github: <span><a href={source_url} rel='noopener noreferrer' target='_blank'>{repository}</a> (v{version})</span>, forge: <span ><a
href={source_url}
rel='noopener noreferrer'
target='_blank'
>{repository}</a > (v{version})</span >,
}} }}
/> />
</p> </p >
</div> </div >
); );
} }

View File

@ -14,158 +14,214 @@ const NavigationPanel = () => (
<div className='navigation-panel'> <div className='navigation-panel'>
<NavLink <NavLink
className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' className='column-link column-link--transparent'
to='/timelines/home'
data-preview-title-id='column.home'
data-preview-icon='home' data-preview-icon='home'
><Icon className='column-link__icon' id='home' fixedWidth/><FormattedMessage ><Icon
id='tabs_bar.home' defaultMessage='Home' className='column-link__icon'
/></NavLink> id='home'
fixedWidth
/><FormattedMessage
id='tabs_bar.home'
defaultMessage='Home'
/></NavLink >
<NavLink <NavLink
className='column-link column-link--transparent' to='/notifications' className='column-link column-link--transparent'
data-preview-title-id='column.notifications' data-preview-icon='bell' to='/notifications'
data-preview-title-id='column.notifications'
data-preview-icon='bell'
><NotificationsCounterIcon ><NotificationsCounterIcon
className='column-link__icon' className='column-link__icon'
/><FormattedMessage /><FormattedMessage
id='tabs_bar.notifications' id='tabs_bar.notifications'
defaultMessage='Notifications' defaultMessage='Notifications'
/></NavLink> /></NavLink >
<FollowRequestsNavLink/> <FollowRequestsNavLink />
<NavLink <NavLink
className='column-link column-link--transparent' to='/timelines/public/local' className='column-link column-link--transparent'
data-preview-title-id='column.community' data-preview-icon='users' to='/timelines/public/local'
data-preview-title-id='column.community'
data-preview-icon='users'
><Icon ><Icon
className='column-link__icon' className='column-link__icon'
id='users' id='users'
fixedWidth fixedWidth
/><FormattedMessage /><FormattedMessage
id='tabs_bar.local_timeline' defaultMessage='Local' id='tabs_bar.local_timeline'
/></NavLink> defaultMessage='Local'
/></NavLink >
<NavLink <NavLink
className='column-link column-link--transparent' exact to='/timelines/public' className='column-link column-link--transparent'
data-preview-title-id='column.public' data-preview-icon='globe' exact
to='/timelines/public'
data-preview-title-id='column.public'
data-preview-icon='globe'
><Icon ><Icon
className='column-link__icon' className='column-link__icon'
id='globe' id='globe'
fixedWidth fixedWidth
/><FormattedMessage /><FormattedMessage
id='tabs_bar.federated_timeline' defaultMessage='Federated' id='tabs_bar.federated_timeline'
/></NavLink> defaultMessage='Federated'
<NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon /></NavLink >
<NavLink
className='column-link column-link--transparent'
to='/timelines/direct'
><Icon
className='column-link__icon' className='column-link__icon'
id='envelope' id='envelope'
fixedWidth fixedWidth
/><FormattedMessage /><FormattedMessage
id='navigation_bar.direct' defaultMessage='Direct messages' id='navigation_bar.direct'
/></NavLink> defaultMessage='Direct messages'
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon /></NavLink >
<NavLink
className='column-link column-link--transparent'
to='/favourites'
><Icon
className='column-link__icon' className='column-link__icon'
id='star' id='star'
fixedWidth fixedWidth
/><FormattedMessage /><FormattedMessage
id='navigation_bar.favourites' defaultMessage='Favourites' id='navigation_bar.favourites'
/></NavLink> defaultMessage='Favourites'
<NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon /></NavLink >
<NavLink
className='column-link column-link--transparent'
to='/bookmarks'
><Icon
className='column-link__icon' className='column-link__icon'
id='bookmark' id='bookmark'
fixedWidth fixedWidth
/><FormattedMessage /><FormattedMessage
id='navigation_bar.bookmarks' defaultMessage='Bookmarks' id='navigation_bar.bookmarks'
/></NavLink> defaultMessage='Bookmarks'
<NavLink className='column-link column-link--transparent' to='/lists'><Icon /></NavLink >
<NavLink
className='column-link column-link--transparent'
to='/lists'
><Icon
className='column-link__icon' className='column-link__icon'
id='list-ul' id='list-ul'
fixedWidth fixedWidth
/><FormattedMessage /><FormattedMessage
id='navigation_bar.lists' defaultMessage='Lists' id='navigation_bar.lists'
/></NavLink> defaultMessage='Lists'
/></NavLink >
{profile_directory && {profile_directory &&
<NavLink className='column-link column-link--transparent' to='/directory'><Icon <NavLink
className='column-link column-link--transparent'
to='/directory'
><Icon
className='column-link__icon' className='column-link__icon'
id='address-book-o' id='address-book-o'
fixedWidth fixedWidth
/><FormattedMessage /><FormattedMessage
id='getting_started.directory' defaultMessage='Profile directory' id='getting_started.directory'
/></NavLink>} defaultMessage='Profile directory'
/></NavLink >}
<ListPanel/> <ListPanel />
<hr/> <hr />
<a className='column-link column-link--transparent' href='/settings/preferences'><Icon <a
className='column-link column-link--transparent'
href='/settings/preferences'
><Icon
className='column-link__icon' className='column-link__icon'
id='cog' id='cog'
fixedWidth fixedWidth
/><FormattedMessage /><FormattedMessage
id='navigation_bar.preferences' defaultMessage='Preferences' id='navigation_bar.preferences'
/></a> defaultMessage='Preferences'
<a className='column-link column-link--transparent' href='/relationships'><Icon /></a >
<a
className='column-link column-link--transparent'
href='/relationships'
><Icon
className='column-link__icon' className='column-link__icon'
id='users' id='users'
fixedWidth fixedWidth
/><FormattedMessage /><FormattedMessage
id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' id='navigation_bar.follows_and_followers'
/></a> defaultMessage='Follows and followers'
/></a >
{showTrends && <div className='flex-spacer'/>} {showTrends && <div className='flex-spacer' />}
{showTrends && <TrendsContainer/>} {showTrends && <TrendsContainer />}
<div className='messaging-box'> <div className='messaging-box'>
<div className='title'> <div className='title'>
<i role='img' className='fa fa-envelope column-header__icon fa-fw'/> <i
role='img'
className='fa fa-envelope column-header__icon fa-fw'
/>
Messaging box Messaging box
</div> </div >
<div className='user-list column-header'> <div className='user-list column-header'>
<h2 className='title'>User list</h2> <h2 className='title'>User list</h2 >
<ul> <ul >
<li> <li >
someone is logged in, click on me someone is logged in, click on me
</li> </li >
<li> <li >
wulfila is here, click on me wulfila is here, click on me
</li> </li >
<li> <li >
chuck norris is here, click on me chuck norris is here, click on me
</li> </li >
</ul> </ul >
</div> </div >
</div> </div >
<div className='conversations_list'> <div className='conversations_list'>
<ul> <ul >
<li className='conversations_item has-new-message'> <li className='conversations_item has-new-message'>
<div className='title'> <div className='title'>
<i role='img' className='fa fa-envelope column-header__icon fa-fw'/> <i
role='img'
className='fa fa-envelope column-header__icon fa-fw'
/>
Un Gens Un Gens
<span className='new-message-counter'> <span className='new-message-counter'>
(3)</span> (3)</span >
<button className='btn-small'> <button className='btn-small'>
<i role='img' className='fa fa-caret-down column-header__icon fa-fw'/> <i
</button> role='img'
</div> className='fa fa-caret-down column-header__icon fa-fw'
/>
</button >
</div >
<div className='conversation_stream'> <div className='conversation_stream'>
<div className='message theirs'> <div className='message theirs'>
<p>oh hello there! 😋 </p> <p >oh hello there! 😋 </p >
</div> <div className='arrow-down' />
</div >
<div className='message mine'> <div className='message mine'>
<p>General Emoji</p> <p >General Emoji</p >
</div> <div className='arrow-down' />
</div >
<div className='message theirs'> <div className='message theirs'>
<p>we just achieved comedy</p> <p >we just achieved comedy</p >
</div> <div className='arrow-down' />
</div> </div >
</div >
<div className='conversation_input'> <div className='conversation_input'>
{/*<form action="#" onSubmit={submitCompose()}>*/} {/*<form action="#" onSubmit={submitCompose()}>*/}
{/* <input type='text' name='compose'/>*/} {/* <input type='text' name='compose'/>*/}
{/* <input type='submit' name='submit'/>*/} {/* <input type='submit' name='submit'/>*/}
{/*</form>*/} {/*</form>*/}
<ComposeFormContainer singleColumn/> <ComposeFormContainer singleColumn />
</div> </div >
{/*<ConversationSmall></ConversationSmall>*/} {/*<ConversationSmall></ConversationSmall>*/}
</li> </li >
</ul> </ul >
</div> </div >
</div> </div >
); );
export default withRouter(NavigationPanel); export default withRouter(NavigationPanel);

View File

@ -853,11 +853,19 @@
background: transparent; background: transparent;
padding: 0; padding: 0;
padding-top: 8px; padding-top: 8px;
text-align: right;
width: 100%;
&:hover, &:hover,
&:active { &:active {
text-decoration: underline; text-decoration: underline;
} }
img {
transform: rotateY(180deg);
margin-left: 1ch;
}
} }
.status__content__spoiler-link { .status__content__spoiler-link {
@ -875,6 +883,13 @@
vertical-align: middle; vertical-align: middle;
} }
.status__wrapper {
&:hover {
background: mix($ui-base-lighter-color, $ui-base-color);
animation: background ease 0.5s;
}
}
.status__wrapper--filtered { .status__wrapper--filtered {
color: $dark-text-color; color: $dark-text-color;
border: 0; border: 0;

View File

@ -137,7 +137,6 @@
} }
.getting-started__footer { .getting-started__footer {
text-align: justify;
ul { ul {
list-style-type: none; list-style-type: none;

View File

@ -30,6 +30,10 @@ $messagingBoxHeight: 20em;
} }
} }
.conversation_created-at {
margin-right: 1em;
}
.conversation_stream { .conversation_stream {
padding-top: 1em; padding-top: 1em;
height: $messagingBoxHeight; height: $messagingBoxHeight;
@ -42,16 +46,39 @@ $messagingBoxHeight: 20em;
border-radius: 0.5rem; border-radius: 0.5rem;
margin-bottom: 0.5em; margin-bottom: 0.5em;
padding: 0.5em 1em; padding: 0.5em 1em;
width: 80%;
} }
.mine { .mine {
text-align: right; text-align: right;
background: $classic-primary-color; background: $classic-primary-color;
float: right;
.arrow-down {
border-top-color: $classic-primary-color;
left: 1em;
}
} }
.theirs { .theirs {
text-align: left; text-align: left;
background: $ui-highlight-color; background: $ui-highlight-color;
float: left;
.arrow-down {
border-top-color: $ui-highlight-color;
right: 1em;
}
}
.arrow-down {
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 20px solid transparent;
border-top: 20px solid $classic-primary-color;
position: relative;
bottom: -1em;
} }
} }

View File

@ -20,3 +20,11 @@
- else - else
= table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}") = table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}")
= table_link_to 'globe', t('admin.accounts.public'), ActivityPub::TagManager.instance.url_for(account) = table_link_to 'globe', t('admin.accounts.public'), ActivityPub::TagManager.instance.url_for(account)
%td
= number_with_delimiter account.statuses_count
%td
= number_with_delimiter account.following_count
%td
= number_with_delimiter account.followers_count
%td
\-

View File

@ -44,10 +44,10 @@
%th= t('admin.accounts.most_recent_ip') %th= t('admin.accounts.most_recent_ip')
%th= t('admin.accounts.most_recent_activity') %th= t('admin.accounts.most_recent_activity')
%th links %th links
%th sign in
%th statuses %th statuses
%th following %th following
%th followers %th followers
%th nuke
%tbody %tbody
= render @accounts = render @accounts