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

View File

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

View File

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

View File

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

View File

@ -5,36 +5,36 @@ import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import { me, isStaff } from '../../../initial_state';
import { isStaff, me } from '../../../initial_state';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
delete : { id: 'status.delete', defaultMessage: 'Delete' },
redraft : { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
direct : { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention : { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply : { id: 'status.reply', defaultMessage: 'Reply' },
reblog : { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private : { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
block: { id: 'status.block', defaultMessage: 'Block @{name}' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
share: { id: 'status.share', defaultMessage: 'Share' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
cannot_reblog : { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite : { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark : { id: 'status.bookmark', defaultMessage: 'Bookmark' },
mute : { id: 'status.mute', defaultMessage: 'Mute @{name}' },
muteConversation : { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation : { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
block : { id: 'status.block', defaultMessage: 'Block @{name}' },
report : { id: 'status.report', defaultMessage: 'Report @{name}' },
share : { id: 'status.share', defaultMessage: 'Share' },
pin : { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin : { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed : { id: 'status.embed', defaultMessage: 'Embed' },
admin_account : { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status : { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy : { id: 'status.copy', defaultMessage: 'Copy link to status' },
blockDomain : { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
unblockDomain : { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
unmute : { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock : { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
});
const mapStateToProps = (state, { status }) => ({
@ -50,59 +50,59 @@ class ActionBar extends React.PureComponent {
};
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map,
onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onMute: PropTypes.func,
onUnmute: PropTypes.func,
onBlock: PropTypes.func,
onUnblock: PropTypes.func,
onBlockDomain: PropTypes.func,
onUnblockDomain: PropTypes.func,
status : ImmutablePropTypes.map.isRequired,
relationship : ImmutablePropTypes.map,
onReply : PropTypes.func.isRequired,
onReblog : PropTypes.func.isRequired,
onFavourite : PropTypes.func.isRequired,
onBookmark : PropTypes.func.isRequired,
onDelete : PropTypes.func.isRequired,
onDirect : PropTypes.func.isRequired,
onMention : PropTypes.func.isRequired,
onMute : PropTypes.func,
onUnmute : PropTypes.func,
onBlock : PropTypes.func,
onUnblock : PropTypes.func,
onBlockDomain : PropTypes.func,
onUnblockDomain : PropTypes.func,
onMuteConversation: PropTypes.func,
onReport: PropTypes.func,
onPin: PropTypes.func,
onEmbed: PropTypes.func,
intl: PropTypes.object.isRequired,
onReport : PropTypes.func,
onPin : PropTypes.func,
onEmbed : PropTypes.func,
intl : PropTypes.object.isRequired,
};
handleReplyClick = () => {
this.props.onReply(this.props.status);
}
};
handleReblogClick = (e) => {
this.props.onReblog(this.props.status, e);
}
};
handleFavouriteClick = () => {
this.props.onFavourite(this.props.status);
}
};
handleBookmarkClick = (e) => {
this.props.onBookmark(this.props.status, e);
}
};
handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history);
}
};
handleRedraftClick = () => {
this.props.onDelete(this.props.status, this.context.router.history, true);
}
};
handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
}
};
handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history);
}
};
handleMuteClick = () => {
const { status, relationship, onMute, onUnmute } = this.props;
@ -113,7 +113,7 @@ class ActionBar extends React.PureComponent {
} else {
onMute(account);
}
}
};
handleBlockClick = () => {
const { status, relationship, onBlock, onUnblock } = this.props;
@ -124,50 +124,50 @@ class ActionBar extends React.PureComponent {
} else {
onBlock(status);
}
}
};
handleBlockDomain = () => {
const { status, onBlockDomain } = this.props;
const account = status.get('account');
onBlockDomain(account.get('acct').split('@')[1]);
}
};
handleUnblockDomain = () => {
const { status, onUnblockDomain } = this.props;
const account = status.get('account');
onUnblockDomain(account.get('acct').split('@')[1]);
}
};
handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status);
}
};
handleReport = () => {
this.props.onReport(this.props.status);
}
};
handlePinClick = () => {
this.props.onPin(this.props.status);
}
};
handleShare = () => {
navigator.share({
text: this.props.status.get('search_index'),
url: this.props.status.get('url'),
url : this.props.status.get('url'),
});
}
};
handleEmbed = () => {
this.props.onEmbed(this.props.status);
}
};
handleCopy = () => {
const url = this.props.status.get('url');
const url = this.props.status.get('url');
const textarea = document.createElement('textarea');
textarea.textContent = url;
textarea.textContent = url;
textarea.style.position = 'fixed';
document.body.appendChild(textarea);
@ -180,14 +180,14 @@ class ActionBar extends React.PureComponent {
} finally {
document.body.removeChild(textarea);
}
}
};
render () {
render() {
const { status, relationship, intl } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const mutingConversation = status.get('muted');
const account = status.get('account');
const account = status.get('account');
let menu = [];
@ -199,36 +199,66 @@ class ActionBar extends React.PureComponent {
if (me === status.getIn(['account', 'id'])) {
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 {
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({ 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({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
menu.push({
text : intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }),
action: this.handleMentionClick,
});
menu.push({
text : intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }),
action: this.handleDirectClick,
});
menu.push(null);
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 {
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')) {
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 {
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')) {
const domain = account.get('acct').split('@')[1];
@ -244,13 +274,23 @@ class ActionBar extends React.PureComponent {
if (isStaff) {
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({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
menu.push({
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' && (
<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;
@ -268,16 +308,48 @@ class ActionBar extends React.PureComponent {
return (
<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 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>
<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
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}
<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'>
<DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title='More' />
</div>
</div>
<DropdownMenuContainer
size={18}
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 ImmutablePropTypes from 'react-immutable-proptypes';
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 DetailedStatus from './components/detailed_status';
import ActionBar from './components/action_bar';
import Column from '../ui/components/column';
import {
favourite,
unfavourite,
bookmark,
unbookmark,
reblog,
unreblog,
favourite,
pin,
reblog,
unbookmark,
unfavourite,
unpin,
unreblog,
} from '../../actions/interactions';
import {
replyCompose,
mentionCompose,
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 { directCompose, mentionCompose, replyCompose } from '../../actions/compose';
import { unblockAccount, unmuteAccount } from '../../actions/accounts';
import { blockDomain, unblockDomain } from '../../actions/domain_blocks';
import { initMuteModal } from '../../actions/mutes';
import { initBlockModal } from '../../actions/blocks';
import { initReport } from '../../actions/reports';
@ -49,24 +32,33 @@ import ColumnBackButton from '../../components/column_back_button';
import ColumnHeader from '../../components/column_header';
import StatusContainer from '../../containers/status_container';
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 { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal } from '../../initial_state';
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';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
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.' },
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?' },
deleteConfirm : { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage : {
id : 'confirmations.delete.message',
defaultMessage: 'Are you sure you want to delete this status?',
},
redraftConfirm : { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
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.',
},
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' },
});
@ -99,7 +91,7 @@ const makeMapStateToProps = () => {
const ids = [statusId];
while (ids.length > 0) {
let id = ids.shift();
let id = ids.shift();
const replies = contextReplies.get(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'));
if (insertAt !== -1) {
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(insertAt, 0, id);
insertAt += 1;
@ -142,7 +135,7 @@ const makeMapStateToProps = () => {
ancestorsIds,
descendantsIds,
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 = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
status: ImmutablePropTypes.map,
ancestorsIds: ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
params : PropTypes.object.isRequired,
dispatch : PropTypes.func.isRequired,
status : ImmutablePropTypes.map,
ancestorsIds : ImmutablePropTypes.list,
descendantsIds : ImmutablePropTypes.list,
intl : PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool,
domain: PropTypes.string.isRequired,
multiColumn : PropTypes.bool,
domain : PropTypes.string.isRequired,
};
state = {
fullscreen: false,
showMedia: defaultMediaVisibility(this.props.status),
fullscreen : false,
showMedia : defaultMediaVisibility(this.props.status),
loadedStatusId: undefined,
};
componentWillMount () {
componentWillMount() {
this.props.dispatch(fetchStatus(this.props.params.statusId));
}
componentDidMount () {
componentDidMount() {
attachFullscreenListener(this.onFullScreenChange);
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this._scrolledIntoView = false;
this.props.dispatch(fetchStatus(nextProps.params.statusId));
}
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 = () => {
this.setState({ showMedia: !this.state.showMedia });
}
};
handleFavouriteClick = (status) => {
if (status.get('favourited')) {
@ -204,7 +200,7 @@ class Status extends ImmutablePureComponent {
} else {
this.props.dispatch(favourite(status));
}
}
};
handlePin = (status) => {
if (status.get('pinned')) {
@ -212,24 +208,24 @@ class Status extends ImmutablePureComponent {
} else {
this.props.dispatch(pin(status));
}
}
};
handleReplyClick = (status) => {
let { askReplyConfirmation, dispatch, intl } = this.props;
if (askReplyConfirmation) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
message : intl.formatMessage(messages.replyMessage),
confirm : intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, this.context.router.history)),
}));
} else {
dispatch(replyCompose(status, this.context.router.history));
}
}
};
handleModalReblog = (status) => {
this.props.dispatch(reblog(status));
}
};
handleReblogClick = (status, e) => {
if (status.get('reblogged')) {
@ -241,7 +237,7 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
}
}
}
};
handleBookmarkClick = (status) => {
if (status.get('bookmarked')) {
@ -249,7 +245,7 @@ class Status extends ImmutablePureComponent {
} else {
this.props.dispatch(bookmark(status));
}
}
};
handleDeleteClick = (status, history, withRedraft = false) => {
const { dispatch, intl } = this.props;
@ -258,28 +254,28 @@ class Status extends ImmutablePureComponent {
dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
message : intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm : intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
}));
}
}
};
handleDirectClick = (account, router) => {
this.props.dispatch(directCompose(account, router));
}
};
handleMentionClick = (account, router) => {
this.props.dispatch(mentionCompose(account, router));
}
};
handleOpenMedia = (media, index) => {
this.props.dispatch(openModal('MEDIA', { media, index }));
}
};
handleOpenVideo = (media, time) => {
this.props.dispatch(openModal('VIDEO', { media, time }));
}
};
handleHotkeyOpenMedia = e => {
const status = this._properStatus();
@ -295,11 +291,11 @@ class Status extends ImmutablePureComponent {
this.handleOpenMedia(status.get('media_attachments'), 0);
}
}
}
};
handleMuteClick = (account) => {
this.props.dispatch(initMuteModal(account));
}
};
handleConversationMuteClick = (status) => {
if (status.get('muted')) {
@ -307,7 +303,7 @@ class Status extends ImmutablePureComponent {
} else {
this.props.dispatch(muteStatus(status.get('id')));
}
}
};
handleToggleHidden = (status) => {
if (status.get('hidden')) {
@ -315,7 +311,7 @@ class Status extends ImmutablePureComponent {
} else {
this.props.dispatch(hideStatus(status.get('id')));
}
}
};
handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds } = this.props;
@ -326,80 +322,83 @@ class Status extends ImmutablePureComponent {
} else {
this.props.dispatch(hideStatus(statusIds));
}
}
};
handleBlockClick = (status) => {
const { dispatch } = this.props;
const account = status.get('account');
dispatch(initBlockModal(account));
}
};
handleReport = (status) => {
this.props.dispatch(initReport(status.get('account'), status));
}
};
handleEmbed = (status) => {
this.props.dispatch(openModal('EMBED', { url: status.get('url') }));
}
};
handleUnmuteClick = account => {
this.props.dispatch(unmuteAccount(account.get('id')));
}
};
handleUnblockClick = account => {
this.props.dispatch(unblockAccount(account.get('id')));
}
};
handleBlockDomainClick = domain => {
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> }} />,
confirm: this.props.intl.formatMessage(messages.blockDomainConfirm),
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 > }}
/>,
confirm : this.props.intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => this.props.dispatch(blockDomain(domain)),
}));
}
};
handleUnblockDomainClick = domain => {
this.props.dispatch(unblockDomain(domain));
}
};
handleHotkeyMoveUp = () => {
this.handleMoveUp(this.props.status.get('id'));
}
};
handleHotkeyMoveDown = () => {
this.handleMoveDown(this.props.status.get('id'));
}
};
handleHotkeyReply = e => {
e.preventDefault();
this.handleReplyClick(this.props.status);
}
};
handleHotkeyFavourite = () => {
this.handleFavouriteClick(this.props.status);
}
};
handleHotkeyBoost = () => {
this.handleReblogClick(this.props.status);
}
};
handleHotkeyMention = e => {
e.preventDefault();
this.handleMentionClick(this.props.status.get('account'));
}
};
handleHotkeyOpenProfile = () => {
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
}
};
handleHotkeyToggleHidden = () => {
this.handleToggleHidden(this.props.status);
}
};
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
}
};
handleMoveUp = id => {
const { status, ancestorsIds, descendantsIds } = this.props;
@ -416,7 +415,7 @@ class Status extends ImmutablePureComponent {
this._selectChild(index - 1, true);
}
}
}
};
handleMoveDown = id => {
const { status, ancestorsIds, descendantsIds } = this.props;
@ -433,9 +432,9 @@ class Status extends ImmutablePureComponent {
this._selectChild(index + 1, false);
}
}
}
};
_selectChild (index, align_top) {
_selectChild(index, align_top) {
const container = this.node;
const element = container.querySelectorAll('.focusable')[index];
@ -449,7 +448,7 @@ class Status extends ImmutablePureComponent {
}
}
renderChildren (list) {
renderChildren(list) {
return list.map(id => (
<StatusContainer
key={id}
@ -463,9 +462,9 @@ class Status extends ImmutablePureComponent {
setRef = c => {
this.node = c;
}
};
componentDidUpdate () {
componentDidUpdate() {
if (this._scrolledIntoView) {
return;
}
@ -482,65 +481,84 @@ class Status extends ImmutablePureComponent {
}
}
componentWillUnmount () {
componentWillUnmount() {
detachFullscreenListener(this.onFullScreenChange);
}
onFullScreenChange = () => {
this.setState({ fullscreen: isFullscreen() });
}
};
render () {
render() {
let ancestors, descendants;
const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn } = this.props;
const { fullscreen } = this.state;
if (status === null) {
return (
<Column>
<Column >
<ColumnBackButton multiColumn={multiColumn} />
<MissingIndicator />
</Column>
</Column >
);
}
if (ancestorsIds && ancestorsIds.size > 0) {
ancestors = <div>{this.renderChildren(ancestorsIds)}</div>;
ancestors = <div >{this.renderChildren(ancestorsIds)}</div >;
}
if (descendantsIds && descendantsIds.size > 0) {
descendants = <div>{this.renderChildren(descendantsIds)}</div>;
descendants = <div >{this.renderChildren(descendantsIds)}</div >;
}
const handlers = {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
reply: this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost,
mention: this.handleHotkeyMention,
openProfile: this.handleHotkeyOpenProfile,
toggleHidden: this.handleHotkeyToggleHidden,
moveUp : this.handleHotkeyMoveUp,
moveDown : this.handleHotkeyMoveDown,
reply : this.handleHotkeyReply,
favourite : this.handleHotkeyFavourite,
boost : this.handleHotkeyBoost,
mention : this.handleHotkeyMention,
openProfile : this.handleHotkeyOpenProfile,
toggleHidden : this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
openMedia : this.handleHotkeyOpenMedia,
};
return (
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.detailedStatus)}>
<Column
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.detailedStatus)}
>
<ColumnHeader
showBackButton
multiColumn={multiColumn}
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}>
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
<ScrollContainer
scrollKey='thread'
shouldUpdateScroll={shouldUpdateScroll}
>
<div
className={classNames('scrollable', { fullscreen })}
ref={this.setRef}
>
{ancestors}
<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
status={status}
onOpenVideo={this.handleOpenVideo}
@ -571,13 +589,13 @@ class Status extends ImmutablePureComponent {
onPin={this.handlePin}
onEmbed={this.handleEmbed}
/>
</div>
</HotKeys>
</div >
</HotKeys >
{descendants}
</div>
</ScrollContainer>
</Column>
</div >
</ScrollContainer >
</Column >
);
}

View File

@ -46,75 +46,123 @@ class LinkFooter extends React.PureComponent {
return (
<div className='getting-started__footer'>
<ul>
<li>
<a href="https://liberapay.com/cipherbliss">Supportez Cipherbliss</a>
</li>
<li>
<a href="/@tykayn">
<i className="fa fa-envelope"></i>
contactez Cipherbliss</a>
</li>
<li>
<ul >
<li >
<a href='https://liberapay.com/cipherbliss'>Supportez Cipherbliss</a >
</li >
<li >
<a href='https://mastodon.cipherbliss.com/@tykayn'>
<i className='fa fa-paper-plane' />
contactez nous</a >
</li >
<li >
<a href='/admin/tags?pending_review=1'>
<i className="fa fa-fire"></i>
Trending hashtags</a>
<hr/>
</li>
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage
<i className='fa fa-fire' />
Trending hashtags</a >
<hr />
</li >
{invitesEnabled && <li ><a
href='/invites'
target='_blank'
><FormattedMessage
id='getting_started.invite'
defaultMessage='Invite people'
/></a> ·
</li>}
{withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage
id='navigation_bar.keyboard_shortcuts'
defaultMessage='Hotkeys'
/></Link> ·
</li>}
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security'/></a> ·
</li>
<li><a href='/about/more' target='_blank'><FormattedMessage
id='navigation_bar.info'
defaultMessage='About this server'
/></a> ·
</li>
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage
id='navigation_bar.apps'
defaultMessage='Mobile apps'
/></a> ·
</li>
<li><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>
/> ·</a >
</li >}
{withHotkeys && <li ><Link to='/keyboard-shortcuts'>
<FormattedMessage
id='navigation_bar.keyboard_shortcuts'
defaultMessage='Hotkeys'
/> ·
</Link >
</li >}
<li >
<a href='/auth/edit'>
<FormattedMessage
id='getting_started.security'
defaultMessage='Security'
/> ·
</a >
</li >
<li >
<a
href='/about/more'
target='_blank'
><FormattedMessage
id='navigation_bar.info'
defaultMessage='About this server'
/> ·
</a >
</li >
<li >
<a
href='https://joinmastodon.org/apps'
target='_blank'
><FormattedMessage
id='navigation_bar.apps'
defaultMessage='Mobile apps'
/> ·
</a >
</li >
<li >
<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
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={{
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>
</div>
</p >
</div >
);
}

View File

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

View File

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

View File

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

View File

@ -30,6 +30,10 @@ $messagingBoxHeight: 20em;
}
}
.conversation_created-at {
margin-right: 1em;
}
.conversation_stream {
padding-top: 1em;
height: $messagingBoxHeight;
@ -42,16 +46,39 @@ $messagingBoxHeight: 20em;
border-radius: 0.5rem;
margin-bottom: 0.5em;
padding: 0.5em 1em;
width: 80%;
}
.mine {
text-align: right;
background: $classic-primary-color;
float: right;
.arrow-down {
border-top-color: $classic-primary-color;
left: 1em;
}
}
.theirs {
text-align: left;
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
= 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)
%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_activity')
%th links
%th sign in
%th statuses
%th following
%th followers
%th nuke
%tbody
= render @accounts