Merge tag 'v2.9.2' into instance_only_statuses
This commit is contained in:
@ -46,7 +46,7 @@ class ActionBar extends React.PureComponent {
|
||||
return (
|
||||
<div className='compose__action-bar'>
|
||||
<div className='compose__action-bar-dropdown'>
|
||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
<DropdownMenuContainer items={menu} icon='chevron-down' size={16} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
|
||||
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
||||
import AutosuggestInput from '../../../components/autosuggest_input';
|
||||
import PollButtonContainer from '../containers/poll_button_container';
|
||||
import UploadButtonContainer from '../containers/upload_button_container';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
@ -61,6 +62,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
onPickEmoji: PropTypes.func.isRequired,
|
||||
showSearch: PropTypes.bool,
|
||||
anyMedia: PropTypes.bool,
|
||||
singleColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -104,13 +106,23 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
onSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value);
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
|
||||
}
|
||||
|
||||
onSpoilerSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
|
||||
}
|
||||
|
||||
handleChangeSpoilerText = (e) => {
|
||||
this.props.onChangeSpoilerText(e.target.value);
|
||||
}
|
||||
|
||||
handleFocus = () => {
|
||||
if (this.composeForm && !this.props.singleColumn) {
|
||||
this.composeForm.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
// This statement does several things:
|
||||
// - If we're beginning a reply, and,
|
||||
@ -137,7 +149,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
this.autosuggestTextarea.textarea.focus();
|
||||
} else if (this.props.spoiler !== prevProps.spoiler) {
|
||||
if (this.props.spoiler) {
|
||||
this.spoilerText.focus();
|
||||
this.spoilerText.input.focus();
|
||||
} else {
|
||||
this.autosuggestTextarea.textarea.focus();
|
||||
}
|
||||
@ -152,6 +164,10 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
this.spoilerText = c;
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.composeForm = c;
|
||||
};
|
||||
|
||||
handleEmojiPick = (data) => {
|
||||
const { text } = this.props;
|
||||
const position = this.autosuggestTextarea.textarea.selectionStart;
|
||||
@ -174,41 +190,50 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='compose-form'>
|
||||
<div className='compose-form' ref={this.setRef}>
|
||||
<WarningContainer />
|
||||
|
||||
<ReplyIndicatorContainer />
|
||||
|
||||
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span>
|
||||
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoilerText} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} tabIndex={this.props.spoiler ? 0 : -1} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className='compose-form__autosuggest-wrapper'>
|
||||
<AutosuggestTextarea
|
||||
ref={this.setAutosuggestTextarea}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
disabled={disabled}
|
||||
value={this.props.text}
|
||||
onChange={this.handleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
||||
value={this.props.spoilerText}
|
||||
onChange={this.handleChangeSpoilerText}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
disabled={!this.props.spoiler}
|
||||
ref={this.setSpoilerText}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
onPaste={onPaste}
|
||||
autoFocus={!showSearch && !isMobile(window.innerWidth)}
|
||||
onSuggestionSelected={this.onSpoilerSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
id='cw-spoiler-input'
|
||||
className='spoiler-input__input'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AutosuggestTextarea
|
||||
ref={this.setAutosuggestTextarea}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
disabled={disabled}
|
||||
value={this.props.text}
|
||||
onChange={this.handleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
onFocus={this.handleFocus}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
onPaste={onPaste}
|
||||
autoFocus={!showSearch && !isMobile(window.innerWidth)}
|
||||
>
|
||||
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
|
||||
</div>
|
||||
|
||||
<div className='compose-form__modifiers'>
|
||||
<UploadFormContainer />
|
||||
<PollFormContainer />
|
||||
</div>
|
||||
<div className='compose-form__modifiers'>
|
||||
<UploadFormContainer />
|
||||
<PollFormContainer />
|
||||
</div>
|
||||
</AutosuggestTextarea>
|
||||
|
||||
<div className='compose-form__buttons-wrapper'>
|
||||
<div className='compose-form__buttons'>
|
||||
|
@ -20,7 +20,7 @@ export default class NavigationBar extends ImmutablePureComponent {
|
||||
<div className='navigation-bar'>
|
||||
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
|
||||
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
|
||||
<Avatar account={this.props.account} size={40} />
|
||||
<Avatar account={this.props.account} size={48} />
|
||||
</Permalink>
|
||||
|
||||
<div className='navigation-bar__profile'>
|
||||
|
@ -5,6 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import AutosuggestInput from 'mastodon/components/autosuggest_input';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -27,6 +28,10 @@ class Option extends React.PureComponent {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onToggleMultiple: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@ -38,12 +43,25 @@ class Option extends React.PureComponent {
|
||||
this.props.onRemove(this.props.index);
|
||||
};
|
||||
|
||||
|
||||
handleToggleMultiple = e => {
|
||||
this.props.onToggleMultiple();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.props.onClearSuggestions();
|
||||
}
|
||||
|
||||
onSuggestionsFetchRequested = (token) => {
|
||||
this.props.onFetchSuggestions(token);
|
||||
}
|
||||
|
||||
onSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isPollMultiple, title, index, intl } = this.props;
|
||||
|
||||
@ -57,12 +75,16 @@ class Option extends React.PureComponent {
|
||||
tabIndex='0'
|
||||
/>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||
maxLength={25}
|
||||
value={title}
|
||||
onChange={this.handleOptionTitleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
/>
|
||||
</label>
|
||||
|
||||
@ -87,6 +109,10 @@ class PollForm extends ImmutablePureComponent {
|
||||
onAddOption: PropTypes.func.isRequired,
|
||||
onRemoveOption: PropTypes.func.isRequired,
|
||||
onChangeSettings: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@ -103,7 +129,7 @@ class PollForm extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props;
|
||||
const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props;
|
||||
|
||||
if (!options) {
|
||||
return null;
|
||||
@ -112,7 +138,7 @@ class PollForm extends ImmutablePureComponent {
|
||||
return (
|
||||
<div className='compose-form__poll-wrapper'>
|
||||
<ul>
|
||||
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} />)}
|
||||
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} {...other} />)}
|
||||
</ul>
|
||||
|
||||
<div className='poll__footer'>
|
||||
|
@ -129,7 +129,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
||||
// It should not be transformed when mounting because the resulting
|
||||
// size will be used to determine the coordinate of the menu by
|
||||
// react-overlays
|
||||
<div className={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}>
|
||||
<div className={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, zIndex: 2 }} role='listbox' ref={this.setRef}>
|
||||
{items.map(item => (
|
||||
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
|
||||
<div className='privacy-dropdown__option__icon'>
|
||||
|
@ -7,6 +7,7 @@ import DisplayName from '../../../components/display_name';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { isRtl } from '../../../rtl';
|
||||
import AttachmentList from 'mastodon/components/attachment_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||
@ -60,6 +61,13 @@ class ReplyIndicator extends ImmutablePureComponent {
|
||||
</div>
|
||||
|
||||
<div className='reply-indicator__content' style={style} dangerouslySetInnerHTML={content} />
|
||||
|
||||
{status.get('media_attachments').size > 0 && (
|
||||
<AttachmentList
|
||||
compact
|
||||
media={status.get('media_attachments')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ class SearchPopout extends React.PureComponent {
|
||||
const { style } = this.props;
|
||||
const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />;
|
||||
return (
|
||||
<div style={{ ...style, position: 'absolute', width: 285 }}>
|
||||
<div style={{ ...style, position: 'absolute', width: 285, zIndex: 2 }}>
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
<div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
||||
@ -47,6 +47,10 @@ class SearchPopout extends React.PureComponent {
|
||||
export default @injectIntl
|
||||
class Search extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
submitted: PropTypes.bool,
|
||||
@ -54,6 +58,7 @@ class Search extends React.PureComponent {
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onShow: PropTypes.func.isRequired,
|
||||
openInRoute: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@ -76,7 +81,12 @@ class Search extends React.PureComponent {
|
||||
handleKeyUp = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onSubmit();
|
||||
|
||||
if (this.props.openInRoute) {
|
||||
this.context.router.history.push('/search');
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
document.querySelector('.ui').parentElement.focus();
|
||||
}
|
||||
|
@ -7,9 +7,11 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
const messages = defineMessages({
|
||||
upload: { id: 'upload_button.label', defaultMessage: 'Add media (JPEG, PNG, GIF, WebM, MP4, MOV)' },
|
||||
upload: { id: 'upload_button.label', defaultMessage: 'Add media ({formats})' },
|
||||
});
|
||||
|
||||
const SUPPORTED_FORMATS = 'JPEG, PNG, GIF, WebM, MP4, MOV, OGG, WAV, MP3, FLAC';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const mapStateToProps = state => ({
|
||||
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
|
||||
@ -60,9 +62,9 @@ class UploadButton extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<div className='compose-form__upload-button'>
|
||||
<IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
|
||||
<IconButton icon='paperclip' title={intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })}</span>
|
||||
<input
|
||||
key={resetFileKey}
|
||||
ref={this.setRef}
|
||||
|
@ -46,8 +46,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(fetchComposeSuggestions(token));
|
||||
},
|
||||
|
||||
onSuggestionSelected (position, token, suggestion) {
|
||||
dispatch(selectComposeSuggestion(position, token, suggestion));
|
||||
onSuggestionSelected (position, token, suggestion, path) {
|
||||
dispatch(selectComposeSuggestion(position, token, suggestion, path));
|
||||
},
|
||||
|
||||
onChangeSpoilerText (checked) {
|
||||
|
@ -1,8 +1,14 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PollForm from '../components/poll_form';
|
||||
import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose';
|
||||
import {
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions,
|
||||
selectComposeSuggestion,
|
||||
} from '../../../actions/compose';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
suggestions: state.getIn(['compose', 'suggestions']),
|
||||
options: state.getIn(['compose', 'poll', 'options']),
|
||||
expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
|
||||
isMultiple: state.getIn(['compose', 'poll', 'multiple']),
|
||||
@ -24,6 +30,19 @@ const mapDispatchToProps = dispatch => ({
|
||||
onChangeSettings(expiresIn, isMultiple) {
|
||||
dispatch(changePollSettings(expiresIn, isMultiple));
|
||||
},
|
||||
|
||||
onClearSuggestions () {
|
||||
dispatch(clearComposeSuggestions());
|
||||
},
|
||||
|
||||
onFetchSuggestions (token) {
|
||||
dispatch(fetchComposeSuggestions(token));
|
||||
},
|
||||
|
||||
onSuggestionSelected (position, token, accountId, path) {
|
||||
dispatch(selectComposeSuggestion(position, token, accountId, path));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PollForm);
|
||||
|
@ -3,7 +3,7 @@ import UploadButton from '../components/upload_button';
|
||||
import { uploadCompose } from '../../../actions/compose';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
|
||||
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))),
|
||||
unavailable: state.getIn(['compose', 'poll']) !== null,
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
});
|
||||
|
@ -106,12 +106,12 @@ class Compose extends React.PureComponent {
|
||||
<div className='drawer__pager'>
|
||||
{!isSearchPage && <div className='drawer__inner' onFocus={this.onFocus}>
|
||||
<NavigationContainer onClose={this.onBlur} />
|
||||
|
||||
<ComposeFormContainer />
|
||||
{multiColumn && (
|
||||
<div className='drawer__inner__mastodon'>
|
||||
<img alt='' draggable='false' src={mascot || elephantUIPlane} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='drawer__inner__mastodon'>
|
||||
<img alt='' draggable='false' src={mascot || elephantUIPlane} />
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||
|
@ -56,7 +56,7 @@ class FollowRequests extends ImmutablePureComponent {
|
||||
const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column icon='users' heading={intl.formatMessage(messages.heading)}>
|
||||
<Column icon='user-plus' heading={intl.formatMessage(messages.heading)}>
|
||||
<ColumnBackButtonSlim />
|
||||
<ScrollableList
|
||||
scrollKey='follow_requests'
|
||||
|
@ -7,12 +7,12 @@ import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { me, invitesEnabled, version, profile_directory, repository, source_url } from '../../initial_state';
|
||||
import { fetchFollowRequests } from '../../actions/accounts';
|
||||
import { me, profile_directory } from '../../initial_state';
|
||||
import { fetchFollowRequests } from 'mastodon/actions/accounts';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { Link } from 'react-router-dom';
|
||||
import NavigationBar from '../compose/components/navigation_bar';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import LinkFooter from 'mastodon/features/ui/components/link_footer';
|
||||
|
||||
const messages = defineMessages({
|
||||
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||
@ -55,10 +55,16 @@ const badgeDisplay = (number, limit) => {
|
||||
}
|
||||
};
|
||||
|
||||
const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class GettingStarted extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
myAccount: ImmutablePropTypes.map.isRequired,
|
||||
@ -70,7 +76,12 @@ class GettingStarted extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { myAccount, fetchFollowRequests } = this.props;
|
||||
const { myAccount, fetchFollowRequests, multiColumn } = this.props;
|
||||
|
||||
if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
|
||||
this.context.router.history.replace('/timelines/home');
|
||||
return;
|
||||
}
|
||||
|
||||
if (myAccount.get('locked')) {
|
||||
fetchFollowRequests();
|
||||
@ -123,7 +134,7 @@ class GettingStarted extends ImmutablePureComponent {
|
||||
height += 48*3;
|
||||
|
||||
if (myAccount.get('locked')) {
|
||||
navItems.push(<ColumnLink key={i++} icon='users' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
||||
navItems.push(<ColumnLink key={i++} icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
||||
height += 48;
|
||||
}
|
||||
|
||||
@ -155,27 +166,7 @@ class GettingStarted extends ImmutablePureComponent {
|
||||
|
||||
{!multiColumn && <div className='flex-spacer' />}
|
||||
|
||||
<div className='getting-started__footer'>
|
||||
<ul>
|
||||
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
|
||||
{multiColumn && <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' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='getting_started.open_source_notice'
|
||||
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
|
||||
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<LinkFooter withHotkeys={multiColumn} />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
|
@ -60,6 +60,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
|
||||
<td><kbd>x</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>h</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.toggle_sensitivity' defaultMessage='to show/hide media' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>up</kbd>, <kbd>k</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td>
|
||||
|
@ -75,6 +75,23 @@ class ListTimeline extends React.PureComponent {
|
||||
this.disconnect = dispatch(connectListStream(id));
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = nextProps.params;
|
||||
|
||||
if (id !== this.props.params.id) {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
|
||||
dispatch(fetchList(id));
|
||||
dispatch(expandListTimeline(id));
|
||||
|
||||
this.disconnect = dispatch(connectListStream(id));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
@ -158,8 +175,6 @@ class ListTimeline extends React.PureComponent {
|
||||
<Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
</ColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
|
@ -65,11 +65,11 @@ class Lists extends ImmutablePureComponent {
|
||||
|
||||
<NewListForm />
|
||||
|
||||
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
|
||||
<ScrollableList
|
||||
scrollKey='lists'
|
||||
shouldUpdateScroll={shouldUpdateScroll}
|
||||
emptyMessage={emptyMessage}
|
||||
prepend={<ColumnSubheading text={intl.formatMessage(messages.subheading)} />}
|
||||
>
|
||||
{lists.map(list =>
|
||||
<ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
|
||||
|
17
app/javascript/mastodon/features/search/index.js
Normal file
17
app/javascript/mastodon/features/search/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import SearchContainer from 'mastodon/features/compose/containers/search_container';
|
||||
import SearchResultsContainer from 'mastodon/features/compose/containers/search_results_container';
|
||||
|
||||
const Search = () => (
|
||||
<div className='column search-page'>
|
||||
<SearchContainer />
|
||||
|
||||
<div className='drawer__pager'>
|
||||
<div className='drawer__inner darker'>
|
||||
<SearchResultsContainer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Search;
|
@ -13,7 +13,6 @@ import Video from '../../video';
|
||||
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import PollContainer from 'mastodon/containers/poll_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
local_only: { id: 'status.local_only', defaultMessage: 'This post is only visible by other users of your instance' },
|
||||
@ -35,6 +34,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||
onHeightChange: PropTypes.func,
|
||||
domain: PropTypes.string.isRequired,
|
||||
compact: PropTypes.bool,
|
||||
showMedia: PropTypes.bool,
|
||||
onToggleMediaVisibility: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -112,23 +113,23 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||
outerStyle.height = `${this.state.height}px`;
|
||||
}
|
||||
|
||||
if (status.get('poll')) {
|
||||
media = <PollContainer pollId={status.get('poll')} />;
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const video = status.getIn(['media_attachments', 0]);
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
|
||||
media = (
|
||||
<Video
|
||||
preview={video.get('preview_url')}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
preview={attachment.get('preview_url')}
|
||||
blurhash={attachment.get('blurhash')}
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
width={300}
|
||||
height={150}
|
||||
inline
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
sensitive={status.get('sensitive')}
|
||||
visible={this.props.showMedia}
|
||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@ -139,6 +140,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||
media={status.get('media_attachments')}
|
||||
height={300}
|
||||
onOpenMedia={this.props.onOpenMedia}
|
||||
visible={this.props.showMedia}
|
||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ 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 } from '../../components/status';
|
||||
import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -131,6 +131,8 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
state = {
|
||||
fullscreen: false,
|
||||
showMedia: defaultMediaVisibility(this.props.status),
|
||||
loadedStatusId: undefined,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
@ -146,6 +148,14 @@ class Status extends ImmutablePureComponent {
|
||||
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') });
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleMediaVisibility = () => {
|
||||
this.setState({ showMedia: !this.state.showMedia });
|
||||
}
|
||||
|
||||
handleFavouriteClick = (status) => {
|
||||
@ -312,6 +322,10 @@ class Status extends ImmutablePureComponent {
|
||||
this.handleToggleHidden(this.props.status);
|
||||
}
|
||||
|
||||
handleHotkeyToggleSensitive = () => {
|
||||
this.handleToggleMediaVisibility();
|
||||
}
|
||||
|
||||
handleMoveUp = id => {
|
||||
const { status, ancestorsIds, descendantsIds } = this.props;
|
||||
|
||||
@ -432,6 +446,7 @@ class Status extends ImmutablePureComponent {
|
||||
mention: this.handleHotkeyMention,
|
||||
openProfile: this.handleHotkeyOpenProfile,
|
||||
toggleHidden: this.handleHotkeyToggleHidden,
|
||||
toggleSensitive: this.handleHotkeyToggleSensitive,
|
||||
};
|
||||
|
||||
return (
|
||||
@ -455,6 +470,8 @@ class Status extends ImmutablePureComponent {
|
||||
onOpenMedia={this.handleOpenMedia}
|
||||
onToggleHidden={this.handleToggleHidden}
|
||||
domain={domain}
|
||||
showMedia={this.state.showMedia}
|
||||
onToggleMediaVisibility={this.handleToggleMediaVisibility}
|
||||
/>
|
||||
|
||||
<ActionBar
|
||||
|
@ -9,8 +9,10 @@ import RelativeTimestamp from '../../../components/relative_timestamp';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import AttachmentList from 'mastodon/components/attachment_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||
});
|
||||
|
||||
@ -51,6 +53,7 @@ class BoostModal extends ImmutablePureComponent {
|
||||
|
||||
render () {
|
||||
const { status, intl } = this.props;
|
||||
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal boost-modal'>
|
||||
@ -71,12 +74,19 @@ class BoostModal extends ImmutablePureComponent {
|
||||
</div>
|
||||
|
||||
<StatusContent status={status} />
|
||||
|
||||
{status.get('media_attachments').size > 0 && (
|
||||
<AttachmentList
|
||||
compact
|
||||
media={status.get('media_attachments')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='boost-modal__action-bar'>
|
||||
<div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} /></div>
|
||||
<Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} ref={this.setRef} />
|
||||
<Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -5,7 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
import { links, getIndex, getLink } from './tabs_bar';
|
||||
import TabsBar, { links, getIndex, getLink } from './tabs_bar';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import BundleContainer from '../containers/bundle_container';
|
||||
@ -14,6 +14,8 @@ import DrawerLoading from './drawer_loading';
|
||||
import BundleColumnError from './bundle_column_error';
|
||||
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import ComposePanel from './compose_panel';
|
||||
import NavigationPanel from './navigation_panel';
|
||||
|
||||
import detectPassiveEvents from 'detect-passive-events';
|
||||
import { scrollRight } from '../../../scroll';
|
||||
@ -139,7 +141,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
||||
<ColumnLoading title={title} icon={icon} />;
|
||||
|
||||
return (
|
||||
<div className='columns-area' key={index}>
|
||||
<div className='columns-area columns-area--mobile' key={index}>
|
||||
{view}
|
||||
</div>
|
||||
);
|
||||
@ -163,17 +165,36 @@ class ColumnsArea extends ImmutablePureComponent {
|
||||
if (singleColumn) {
|
||||
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
|
||||
|
||||
return columnIndex !== -1 ? [
|
||||
const content = columnIndex !== -1 ? (
|
||||
<ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
|
||||
{links.map(this.renderView)}
|
||||
</ReactSwipeableViews>,
|
||||
</ReactSwipeableViews>
|
||||
) : (
|
||||
<div key='content' className='columns-area columns-area--mobile'>{children}</div>
|
||||
);
|
||||
|
||||
floatingActionButton,
|
||||
] : [
|
||||
<div className='columns-area'>{children}</div>,
|
||||
return (
|
||||
<div className='columns-area__panels'>
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
<ComposePanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
floatingActionButton,
|
||||
];
|
||||
<div className='columns-area__panels__main'>
|
||||
<TabsBar key='tabs' />
|
||||
{content}
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
<NavigationPanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{floatingActionButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import SearchContainer from 'mastodon/features/compose/containers/search_container';
|
||||
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
|
||||
import NavigationContainer from 'mastodon/features/compose/containers/navigation_container';
|
||||
import LinkFooter from './link_footer';
|
||||
|
||||
const ComposePanel = () => (
|
||||
<div className='compose-panel'>
|
||||
<SearchContainer openInRoute />
|
||||
<NavigationContainer />
|
||||
<ComposeFormContainer singleColumn />
|
||||
<LinkFooter withHotkeys />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ComposePanel;
|
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { fetchFollowRequests } from 'mastodon/actions/accounts';
|
||||
import { connect } from 'react-redux';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
import IconWithBadge from 'mastodon/components/icon_with_badge';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
locked: state.getIn(['accounts', me, 'locked']),
|
||||
count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
|
||||
});
|
||||
|
||||
export default @withRouter
|
||||
@connect(mapStateToProps)
|
||||
class FollowRequestsNavLink extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
locked: PropTypes.bool,
|
||||
count: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, locked } = this.props;
|
||||
|
||||
if (locked) {
|
||||
dispatch(fetchFollowRequests());
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { locked, count } = this.props;
|
||||
|
||||
if (!locked || count === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <NavLink className='column-link column-link--transparent' to='/follow_requests'><IconWithBadge className='column-link__icon' id='user-plus' count={count} /><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></NavLink>;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { invitesEnabled, version, repository, source_url } from 'mastodon/initial_state';
|
||||
|
||||
const LinkFooter = ({ withHotkeys }) => (
|
||||
<div className='getting-started__footer'>
|
||||
<ul>
|
||||
{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' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='getting_started.open_source_notice'
|
||||
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
|
||||
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
LinkFooter.propTypes = {
|
||||
withHotkeys: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default LinkFooter;
|
55
app/javascript/mastodon/features/ui/components/list_panel.js
Normal file
55
app/javascript/mastodon/features/ui/components/list_panel.js
Normal file
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { fetchLists } from 'mastodon/actions/lists';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
||||
if (!lists) {
|
||||
return lists;
|
||||
}
|
||||
|
||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(4);
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
lists: getOrderedLists(state),
|
||||
});
|
||||
|
||||
export default @withRouter
|
||||
@connect(mapStateToProps)
|
||||
class ListPanel extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
lists: ImmutablePropTypes.list,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchLists());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { lists } = this.props;
|
||||
|
||||
if (!lists || lists.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<hr />
|
||||
|
||||
{lists.map(list => (
|
||||
<NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/timelines/list/${list.get('id')}`}><Icon className='column-link__icon' id='list-ul' fixedWidth />{list.get('title')}</NavLink>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { profile_directory } from 'mastodon/initial_state';
|
||||
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||
import FollowRequestsNavLink from './follow_requests_nav_link';
|
||||
import ListPanel from './list_panel';
|
||||
|
||||
const NavigationPanel = () => (
|
||||
<div className='navigation-panel'>
|
||||
<NavLink 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>
|
||||
<NavLink 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 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>
|
||||
<NavLink 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 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 className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></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>
|
||||
|
||||
<ListPanel />
|
||||
|
||||
<hr />
|
||||
|
||||
<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 className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>
|
||||
{!!profile_directory && <a className='column-link column-link--transparent' href='/explore'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></a>}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default withRouter(NavigationPanel);
|
@ -0,0 +1,9 @@
|
||||
import { connect } from 'react-redux';
|
||||
import IconWithBadge from 'mastodon/components/icon_with_badge';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
count: state.getIn(['notifications', 'unread']),
|
||||
id: 'bell',
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(IconWithBadge);
|
@ -5,16 +5,15 @@ import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { debounce } from 'lodash';
|
||||
import { isUserTouching } from '../../../is_mobile';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||
|
||||
export const links = [
|
||||
<NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><Icon id='bell' fixedWidth /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
|
||||
|
||||
<NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link primary' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
|
||||
|
||||
<NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
|
||||
<NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
|
||||
<NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
|
||||
];
|
||||
|
||||
export function getIndex (path) {
|
||||
|
@ -7,7 +7,6 @@ import { Redirect, withRouter } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import NotificationsContainer from './containers/notifications_container';
|
||||
import LoadingBarContainer from './containers/loading_bar_container';
|
||||
import TabsBar from './components/tabs_bar';
|
||||
import ModalContainer from './containers/modal_container';
|
||||
import { isMobile } from '../../is_mobile';
|
||||
import { debounce } from 'lodash';
|
||||
@ -45,8 +44,9 @@ import {
|
||||
Mutes,
|
||||
PinnedStatuses,
|
||||
Lists,
|
||||
Search,
|
||||
} from './util/async-components';
|
||||
import { me } from '../../initial_state';
|
||||
import { me, forceSingleColumn } from '../../initial_state';
|
||||
import { previewState as previewMediaState } from './components/media_modal';
|
||||
import { previewState as previewVideoState } from './components/video_modal';
|
||||
|
||||
@ -93,6 +93,7 @@ const keyMap = {
|
||||
goToMuted: 'g m',
|
||||
goToRequests: 'g r',
|
||||
toggleHidden: 'x',
|
||||
toggleSensitive: 'h',
|
||||
};
|
||||
|
||||
class SwitchingColumnsArea extends React.PureComponent {
|
||||
@ -141,10 +142,11 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||
render () {
|
||||
const { children } = this.props;
|
||||
const { mobile } = this.state;
|
||||
const redirect = mobile ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
|
||||
const singleColumn = forceSingleColumn || mobile;
|
||||
const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
|
||||
|
||||
return (
|
||||
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}>
|
||||
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
|
||||
<WrappedSwitch>
|
||||
{redirect}
|
||||
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
||||
@ -160,7 +162,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||
|
||||
<WrappedRoute path='/search' component={Compose} content={children} componentParams={{ isSearchPage: true }} />
|
||||
<WrappedRoute path='/search' component={Search} content={children} />
|
||||
|
||||
<WrappedRoute path='/statuses/new' component={Compose} content={children} />
|
||||
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||
@ -479,8 +481,6 @@ class UI extends React.PureComponent {
|
||||
return (
|
||||
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
|
||||
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
|
||||
<TabsBar />
|
||||
|
||||
<SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange}>
|
||||
{children}
|
||||
</SwitchingColumnsArea>
|
||||
|
@ -129,3 +129,7 @@ export function ListEditor () {
|
||||
export function ListAdder () {
|
||||
return import(/*webpackChunkName: "features/list_adder" */'../../list_adder');
|
||||
}
|
||||
|
||||
export function Search () {
|
||||
return import(/*webpackChunkName: "features/search" */'../../search');
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { fromJS } from 'immutable';
|
||||
import { fromJS, is } from 'immutable';
|
||||
import { throttle } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
|
||||
@ -102,6 +102,8 @@ class Video extends React.PureComponent {
|
||||
detailed: PropTypes.bool,
|
||||
inline: PropTypes.bool,
|
||||
cacheWidth: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
onToggleVisibility: PropTypes.func,
|
||||
intl: PropTypes.object.isRequired,
|
||||
blurhash: PropTypes.string,
|
||||
link: PropTypes.node,
|
||||
@ -117,7 +119,7 @@ class Video extends React.PureComponent {
|
||||
fullscreen: false,
|
||||
hovered: false,
|
||||
muted: false,
|
||||
revealed: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
|
||||
revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
|
||||
};
|
||||
|
||||
// hard coded in components.scss
|
||||
@ -280,7 +282,16 @@ class Video extends React.PureComponent {
|
||||
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
|
||||
this.setState({ revealed: nextProps.visible });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
if (prevState.revealed && !this.state.revealed && this.video) {
|
||||
this.video.pause();
|
||||
}
|
||||
if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) {
|
||||
this._decode();
|
||||
}
|
||||
@ -316,11 +327,11 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
toggleReveal = () => {
|
||||
if (this.state.revealed) {
|
||||
this.video.pause();
|
||||
if (this.props.onToggleVisibility) {
|
||||
this.props.onToggleVisibility();
|
||||
} else {
|
||||
this.setState({ revealed: !this.state.revealed });
|
||||
}
|
||||
|
||||
this.setState({ revealed: !this.state.revealed });
|
||||
}
|
||||
|
||||
handleLoadedData = () => {
|
||||
|
Reference in New Issue
Block a user