diff options
Diffstat (limited to 'app/javascript/flavours')
41 files changed, 327 insertions, 125 deletions
diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index c6d8486f9..fa8845002 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -79,7 +79,7 @@ export function redraft(status) { }; }; -export function deleteStatus(id, withRedraft = false) { +export function deleteStatus(id, router, withRedraft = false) { return (dispatch, getState) => { const status = getState().getIn(['statuses', id]); @@ -91,6 +91,10 @@ export function deleteStatus(id, withRedraft = false) { if (withRedraft) { dispatch(redraft(status)); + + if (!getState().getIn(['compose', 'mounted'])) { + router.push('/statuses/new'); + } } }).catch(error => { dispatch(deleteStatusFail(id, error)); diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js index b96b4dd98..a677cbf5b 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/components/scrollable_list.js @@ -149,6 +149,10 @@ export default class ScrollableList extends PureComponent { this.props.onLoadMore(); } + defaultShouldUpdateScroll = (prevRouterProps, { location }) => { + return !(location.state && location.state.mastodonModalOpen); + } + render () { const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; @@ -190,7 +194,7 @@ export default class ScrollableList extends PureComponent { if (trackScroll) { return ( - <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}> + <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll || this.defaultShouldUpdateScroll}> {scrollableArea} </ScrollContainer> ); diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 1ac5a4b3e..9f47abfef 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -13,6 +13,7 @@ import { MediaGallery, Video } from 'flavours/glitch/util/async-components'; import { HotKeys } from 'react-hotkeys'; import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container'; import classNames from 'classnames'; +import { autoUnfoldCW } from 'flavours/glitch/util/content_warning'; // We use the component (and not the container) since we do not want // to use the progress bar to show download progress @@ -56,6 +57,7 @@ export default class Status extends ImmutablePureComponent { state = { isCollapsed: false, autoCollapsed: false, + isExpanded: undefined, } // Avoid checking props that are functions (and whose equality will always @@ -123,6 +125,17 @@ export default class Status extends ImmutablePureComponent { updated = true; } + if (nextProps.expanded === undefined && + prevState.isExpanded === undefined && + update.isExpanded === undefined + ) { + const isExpanded = autoUnfoldCW(nextProps.settings, nextProps.status); + if (isExpanded !== undefined) { + update.isExpanded = isExpanded; + updated = true; + } + } + return updated ? update : null; } diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 8a840030a..f7e741d2d 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -5,7 +5,7 @@ import IconButton from './icon_button'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me } from 'flavours/glitch/util/initial_state'; +import { me, isStaff } from 'flavours/glitch/util/initial_state'; import RelativeTimestamp from './relative_timestamp'; const messages = defineMessages({ @@ -31,6 +31,8 @@ const messages = defineMessages({ 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' }, }); const obfuscatedCount = count => { @@ -102,11 +104,11 @@ export default class StatusActionBar extends ImmutablePureComponent { } handleDeleteClick = () => { - this.props.onDelete(this.props.status); + this.props.onDelete(this.props.status, this.context.router.history); } handleRedraftClick = () => { - this.props.onDelete(this.props.status, true); + this.props.onDelete(this.props.status, this.context.router.history, true); } handlePinClick = () => { @@ -186,6 +188,11 @@ export default class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); + 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')}` }); + } } if (status.get('in_reply_to_id', null) === null) { diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 48cb76f86..5ac92ea39 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -122,14 +122,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(openModal('EMBED', { url: status.get('url') })); }, - onDelete (status, withRedraft = false) { + onDelete (status, history, withRedraft = false) { if (!deleteModal) { - dispatch(deleteStatus(status.get('id'), withRedraft)); + 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), - onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)), + onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), })); } }, diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.js b/app/javascript/flavours/glitch/features/account/components/action_bar.js index 9c80a470b..26717ee49 100644 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import { Link } from 'react-router-dom'; import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; -import { me } from 'flavours/glitch/util/initial_state'; +import { me, isStaff } from 'flavours/glitch/util/initial_state'; const messages = defineMessages({ mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, @@ -25,6 +25,7 @@ const messages = defineMessages({ showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, + admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, }); @injectIntl @@ -120,6 +121,11 @@ export default class ActionBar extends React.PureComponent { } } + if (account.get('id') !== me && isStaff) { + menu.push(null); + menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` }); + } + return ( <div> {extraInfo} diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index 20ba0a1b1..2216f9153 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -60,10 +60,6 @@ export default class AccountTimeline extends ImmutablePureComponent { this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { statusIds, featuredStatusIds, isLoading, hasMore } = this.props; @@ -87,7 +83,6 @@ export default class AccountTimeline extends ImmutablePureComponent { isLoading={isLoading} hasMore={hasMore} onLoadMore={this.handleLoadMore} - shouldUpdateScroll={this.shouldUpdateScroll} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js index f1b4f947e..9468ad81d 100644 --- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js +++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js @@ -66,10 +66,6 @@ export default class Bookmarks extends ImmutablePureComponent { this.props.dispatch(expandBookmarkedStatuses()); }, 300, { leading: true }) - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; const pinned = !!columnId; @@ -91,7 +87,6 @@ export default class Bookmarks extends ImmutablePureComponent { trackScroll={!pinned} statusIds={statusIds} scrollKey={`bookmarked_statuses-${columnId}`} - shouldUpdateScroll={this.shouldUpdateScroll} hasMore={hasMore} isLoading={isLoading} onLoadMore={this.handleLoadMore} diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.js index e5006b4d3..b5843ca16 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.js +++ b/app/javascript/flavours/glitch/features/community_timeline/index.js @@ -71,10 +71,6 @@ export default class CommunityTimeline extends React.PureComponent { this.props.dispatch(expandCommunityTimeline({ maxId })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, hasUnread, columnId, multiColumn } = this.props; const pinned = !!columnId; @@ -97,7 +93,6 @@ export default class CommunityTimeline extends React.PureComponent { <StatusListContainer trackScroll={!pinned} scrollKey={`community_timeline-${columnId}`} - shouldUpdateScroll={this.shouldUpdateScroll} timelineId='community' onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js index cf6f45b34..bc409f0a3 100644 --- a/app/javascript/flavours/glitch/features/composer/index.js +++ b/app/javascript/flavours/glitch/features/composer/index.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages } from 'react-intl'; const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\S+)/i; @@ -49,6 +50,13 @@ import { assignHandlers } from 'flavours/glitch/util/react_helpers'; import { wrap } from 'flavours/glitch/util/redux_helpers'; import { privacyPreference } from 'flavours/glitch/util/privacy_preference'; +const messages = defineMessages({ + missingDescriptionMessage: { id: 'confirmations.missing_media_description.message', + defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' }, + missingDescriptionConfirm: { id: 'confirmations.missing_media_description.confirm', + defaultMessage: 'Send anyway' }, +}); + // State mapping. function mapStateToProps (state) { const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']); @@ -93,11 +101,12 @@ function mapStateToProps (state) { text: state.getIn(['compose', 'text']), anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, spoilersAlwaysOn: spoilersAlwaysOn, + mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']), }; }; // Dispatch mapping. -const mapDispatchToProps = (dispatch) => ({ +const mapDispatchToProps = (dispatch, { intl }) => ({ onCancelReply() { dispatch(cancelReplyCompose()); }, @@ -149,6 +158,13 @@ const mapDispatchToProps = (dispatch) => ({ onSelectSuggestion(position, token, suggestion) { dispatch(selectComposeSuggestion(position, token, suggestion)); }, + onMediaDescriptionConfirm() { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.missingDescriptionMessage), + confirm: intl.formatMessage(messages.missingDescriptionConfirm), + onConfirm: () => dispatch(submitCompose()), + })); + }, onSubmit() { dispatch(submitCompose()); }, @@ -206,14 +222,17 @@ const handlers = { // Submits the status. handleSubmit () { - const { textarea: { value } } = this; + const { textarea: { value }, uploadForm } = this; const { onChangeText, onSubmit, isSubmitting, isUploading, + media, anyMedia, text, + mediaDescriptionConfirmation, + onMediaDescriptionConfirm, } = this.props; // If something changes inside the textarea, then we update the @@ -227,12 +246,26 @@ const handlers = { return; } - // Submits the status. - if (onSubmit) { + // Submit unless there are media with missing descriptions + if (mediaDescriptionConfirmation && onMediaDescriptionConfirm && media && media.some(item => !item.get('description'))) { + const firstWithoutDescription = media.findIndex(item => !item.get('description')); + if (uploadForm) { + const inputs = uploadForm.querySelectorAll('.composer--upload_form--item input'); + if (inputs.length == media.size && firstWithoutDescription !== -1) { + inputs[firstWithoutDescription].focus(); + } + } + onMediaDescriptionConfirm(); + } else if (onSubmit) { onSubmit(); } }, + // Sets a reference to the upload form. + handleRefUploadForm (uploadFormComponent) { + this.uploadForm = uploadFormComponent; + }, + // Sets a reference to the textarea. handleRefTextarea (textareaComponent) { if (textareaComponent) { @@ -339,6 +372,7 @@ class Composer extends React.Component { handleSecondarySubmit, handleSelect, handleSubmit, + handleRefUploadForm, handleRefTextarea, handleRefSpoilerText, } = this.handlers; @@ -429,6 +463,7 @@ class Composer extends React.Component { onRemove={onUndoUpload} progress={progress} uploading={isUploading} + handleRef={handleRefUploadForm} /> ) : null} <ComposerOptions @@ -495,6 +530,9 @@ Composer.propTypes = { suggestionToken: PropTypes.string, suggestions: ImmutablePropTypes.list, text: PropTypes.string, + anyMedia: PropTypes.bool, + spoilersAlwaysOn: PropTypes.bool, + mediaDescriptionConfirmation: PropTypes.bool, // Dispatch props. onCancelReply: PropTypes.func, @@ -517,8 +555,7 @@ Composer.propTypes = { onUndoUpload: PropTypes.func, onUnmount: PropTypes.func, onUpload: PropTypes.func, - anyMedia: PropTypes.bool, - spoilersAlwaysOn: PropTypes.bool, + onMediaDescriptionConfirm: PropTypes.func, }; // Connecting and export. diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/index.js index f3cadc2f5..c2ff66623 100644 --- a/app/javascript/flavours/glitch/features/composer/upload_form/index.js +++ b/app/javascript/flavours/glitch/features/composer/upload_form/index.js @@ -17,12 +17,13 @@ export default function ComposerUploadForm ({ onRemove, progress, uploading, + handleRef, }) { const computedClass = classNames('composer--upload_form', { uploading }); // The result. return ( - <div className={computedClass}> + <div className={computedClass} ref={handleRef}> {uploading ? <ComposerUploadFormProgress progress={progress} /> : null} {media ? ( <div className='content'> @@ -55,4 +56,5 @@ ComposerUploadForm.propTypes = { onRemove: PropTypes.func.isRequired, progress: PropTypes.number, uploading: PropTypes.bool, + handleRef: PropTypes.func, }; diff --git a/app/javascript/flavours/glitch/features/direct_timeline/index.js b/app/javascript/flavours/glitch/features/direct_timeline/index.js index 25af49342..418db7c79 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/index.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/index.js @@ -71,10 +71,6 @@ export default class DirectTimeline extends React.PureComponent { this.props.dispatch(expandDirectTimeline({ maxId })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, hasUnread, columnId, multiColumn } = this.props; const pinned = !!columnId; @@ -97,7 +93,6 @@ export default class DirectTimeline extends React.PureComponent { <StatusListContainer trackScroll={!pinned} scrollKey={`direct_timeline-${columnId}`} - shouldUpdateScroll={this.shouldUpdateScroll} timelineId='direct' onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />} diff --git a/app/javascript/flavours/glitch/features/domain_blocks/index.js b/app/javascript/flavours/glitch/features/domain_blocks/index.js index 8b023e0bc..3b29e2a26 100644 --- a/app/javascript/flavours/glitch/features/domain_blocks/index.js +++ b/app/javascript/flavours/glitch/features/domain_blocks/index.js @@ -40,10 +40,6 @@ export default class Blocks extends ImmutablePureComponent { this.props.dispatch(expandDomainBlocks()); }, 300, { leading: true }); - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, domains } = this.props; @@ -58,7 +54,7 @@ export default class Blocks extends ImmutablePureComponent { return ( <Column icon='minus-circle' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> - <ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore} shouldUpdateScroll={this.shouldUpdateScroll}> + <ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore}> {domains.map(domain => <DomainContainer key={domain} domain={domain} /> )} diff --git a/app/javascript/flavours/glitch/features/drawer/index.js b/app/javascript/flavours/glitch/features/drawer/index.js index 1679e9a4b..4649e404f 100644 --- a/app/javascript/flavours/glitch/features/drawer/index.js +++ b/app/javascript/flavours/glitch/features/drawer/index.js @@ -86,6 +86,7 @@ class Drawer extends React.Component { searchHidden, searchValue, submitted, + isSearchPage, } = this.props; const computedClass = classNames('drawer', `mbstobon-${elefriend}`); @@ -99,23 +100,24 @@ class Drawer extends React.Component { onSettingsClick={onOpenSettings} /> ) : null} - <DrawerSearch - intl={intl} - onChange={onChange} - onClear={onClear} - onShow={onShow} - onSubmit={onSubmit} - submitted={submitted} - value={searchValue} - /> + {(multiColumn || isSearchPage) && <DrawerSearch + intl={intl} + onChange={onChange} + onClear={onClear} + onShow={onShow} + onSubmit={onSubmit} + submitted={submitted} + value={searchValue} + /> } <div className='contents'> - <DrawerAccount account={account} /> - <Composer /> + {!isSearchPage && <DrawerAccount account={account} />} + {!isSearchPage && <Composer />} {multiColumn && <button className='mastodon' onClick={onClickElefriend} />} - <DrawerResults - results={results} - visible={submitted && !searchHidden} - /> + {(multiColumn || isSearchPage) && + <DrawerResults + results={results} + visible={submitted && !searchHidden} + />} </div> </div> ); @@ -126,6 +128,7 @@ class Drawer extends React.Component { // Props. Drawer.propTypes = { intl: PropTypes.object.isRequired, + isSearchPage: PropTypes.bool, multiColumn: PropTypes.bool, // State props. diff --git a/app/javascript/flavours/glitch/features/drawer/search/popout/index.js b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js index 6219f46ca..fec090b64 100644 --- a/app/javascript/flavours/glitch/features/drawer/search/popout/index.js +++ b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js @@ -9,6 +9,7 @@ import spring from 'react-motion/lib/spring'; // Utils. import Motion from 'flavours/glitch/util/optional_motion'; +import { searchEnabled } from 'flavours/glitch/util/initial_state'; // Messages. const messages = defineMessages({ @@ -28,6 +29,10 @@ const messages = defineMessages({ defaultMessage: 'Simple text returns matching display names, usernames and hashtags', id: 'search_popout.tips.text', }, + 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.', + id: 'search_popout.tips.full_text', + }, user: { defaultMessage: 'user', id: 'search_popout.tips.user', @@ -92,7 +97,7 @@ export default function DrawerSearchPopout ({ style }) { <FormattedMessage {...messages.status} /> </li> </ul> - <FormattedMessage {...messages.text} /> + { searchEnabled ? <FormattedMessage {...messages.full_text} /> : <FormattedMessage {...messages.text} /> } </div> )} </Motion> diff --git a/app/javascript/flavours/glitch/features/favourited_statuses/index.js b/app/javascript/flavours/glitch/features/favourited_statuses/index.js index 644493183..d8fa1b84e 100644 --- a/app/javascript/flavours/glitch/features/favourited_statuses/index.js +++ b/app/javascript/flavours/glitch/features/favourited_statuses/index.js @@ -66,10 +66,6 @@ export default class Favourites extends ImmutablePureComponent { this.props.dispatch(expandFavouritedStatuses()); }, 300, { leading: true }) - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; const pinned = !!columnId; @@ -91,7 +87,6 @@ export default class Favourites extends ImmutablePureComponent { trackScroll={!pinned} statusIds={statusIds} scrollKey={`favourited_statuses-${columnId}`} - shouldUpdateScroll={this.shouldUpdateScroll} hasMore={hasMore} isLoading={isLoading} onLoadMore={this.handleLoadMore} diff --git a/app/javascript/flavours/glitch/features/favourites/index.js b/app/javascript/flavours/glitch/features/favourites/index.js index 055a15ccb..cf8b31eb3 100644 --- a/app/javascript/flavours/glitch/features/favourites/index.js +++ b/app/javascript/flavours/glitch/features/favourites/index.js @@ -33,6 +33,10 @@ export default class Favourites extends ImmutablePureComponent { } } + shouldUpdateScroll = (prevRouterProps, { location }) => { + return !(location.state && location.state.mastodonModalOpen); + } + render () { const { accountIds } = this.props; @@ -48,7 +52,7 @@ export default class Favourites extends ImmutablePureComponent { <Column> <ColumnBackButton /> - <ScrollContainer scrollKey='favourites'> + <ScrollContainer scrollKey='favourites' shouldUpdateScroll={this.shouldUpdateScroll}> <div className='scrollable'> {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} </div> diff --git a/app/javascript/flavours/glitch/features/follow_requests/index.js b/app/javascript/flavours/glitch/features/follow_requests/index.js index 04ff3f111..1e4633984 100644 --- a/app/javascript/flavours/glitch/features/follow_requests/index.js +++ b/app/javascript/flavours/glitch/features/follow_requests/index.js @@ -42,6 +42,10 @@ export default class FollowRequests extends ImmutablePureComponent { } } + shouldUpdateScroll = (prevRouterProps, { location }) => { + return !(location.state && location.state.mastodonModalOpen); + } + render () { const { intl, accountIds } = this.props; @@ -57,7 +61,7 @@ export default class FollowRequests extends ImmutablePureComponent { <Column name='follow-requests' icon='users' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> - <ScrollContainer scrollKey='follow_requests'> + <ScrollContainer scrollKey='follow_requests' shouldUpdateScroll={this.shouldUpdateScroll}> <div className='scrollable' onScroll={this.handleScroll}> {accountIds.map(id => <AccountAuthorizeContainer key={id} id={id} /> diff --git a/app/javascript/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js index c42e0386c..cdde1775c 100644 --- a/app/javascript/flavours/glitch/features/followers/index.js +++ b/app/javascript/flavours/glitch/features/followers/index.js @@ -56,6 +56,10 @@ export default class Followers extends ImmutablePureComponent { this.props.dispatch(expandFollowers(this.props.params.accountId)); } + shouldUpdateScroll = (prevRouterProps, { location }) => { + return !(location.state && location.state.mastodonModalOpen); + } + render () { const { accountIds, hasMore } = this.props; @@ -77,7 +81,7 @@ export default class Followers extends ImmutablePureComponent { <Column> <ColumnBackButton /> - <ScrollContainer scrollKey='followers'> + <ScrollContainer scrollKey='followers' shouldUpdateScroll={this.shouldUpdateScroll}> <div className='scrollable' onScroll={this.handleScroll}> <div className='followers'> <HeaderContainer accountId={this.props.params.accountId} hideTabs /> diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js index b3e8b7a6e..8f77ed42b 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js @@ -82,10 +82,6 @@ export default class HashtagTimeline extends React.PureComponent { this.props.dispatch(expandHashtagTimeline(this.props.params.id, { maxId })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { hasUnread, columnId, multiColumn } = this.props; const { id } = this.props.params; @@ -110,7 +106,6 @@ export default class HashtagTimeline extends React.PureComponent { scrollKey={`hashtag_timeline-${columnId}`} timelineId={`hashtag:${id}`} onLoadMore={this.handleLoadMore} - shouldUpdateScroll={this.shouldUpdateScroll} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} /> </Column> diff --git a/app/javascript/flavours/glitch/features/local_settings/navigation/index.js b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js index fc2167c0c..0c1040290 100644 --- a/app/javascript/flavours/glitch/features/local_settings/navigation/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js @@ -10,6 +10,7 @@ import LocalSettingsNavigationItem from './item'; const messages = defineMessages({ general: { id: 'settings.general', defaultMessage: 'General' }, + content_warnings: { id: 'settings.content_warnings', defaultMessage: 'Content Warnings' }, collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' }, media: { id: 'settings.media', defaultMessage: 'Media' }, preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' }, @@ -42,25 +43,31 @@ export default class LocalSettingsNavigation extends React.PureComponent { active={index === 1} index={1} onNavigate={onNavigate} - title={intl.formatMessage(messages.collapsed)} + title={intl.formatMessage(messages.content_warnings)} /> <LocalSettingsNavigationItem active={index === 2} index={2} onNavigate={onNavigate} - title={intl.formatMessage(messages.media)} + title={intl.formatMessage(messages.collapsed)} /> <LocalSettingsNavigationItem active={index === 3} - href='/settings/preferences' index={3} + onNavigate={onNavigate} + title={intl.formatMessage(messages.media)} + /> + <LocalSettingsNavigationItem + active={index === 4} + href='/settings/preferences' + index={4} icon='cog' title={intl.formatMessage(messages.preferences)} /> <LocalSettingsNavigationItem - active={index === 4} + active={index === 5} className='close' - index={4} + index={5} onNavigate={onClose} title={intl.formatMessage(messages.close)} /> diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js index ad5c11979..0db49ba5d 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js @@ -17,6 +17,7 @@ const messages = defineMessages({ side_arm_keep: { id: 'settings.side_arm_reply_mode.keep', defaultMessage: 'Keep secondary toot button to set privacy' }, side_arm_copy: { id: 'settings.side_arm_reply_mode.copy', defaultMessage: 'Copy privacy setting of the toot being replied to' }, side_arm_restrict: { id: 'settings.side_arm_reply_mode.restrict', defaultMessage: 'Restrict privacy setting to that of the toot being replied to' }, + regexp: { id: 'settings.content_warnings.regexp', defaultMessage: 'Regular expression' }, }); @injectIntl @@ -85,6 +86,14 @@ export default class LocalSettingsPage extends React.PureComponent { </LocalSettingsPageItem> <LocalSettingsPageItem settings={settings} + item={['confirm_missing_media_description']} + id='mastodon-settings--confirm_missing_media_description' + onChange={onChange} + > + <FormattedMessage id='settings.confirm_missing_media_description' defaultMessage='Show confirmation dialog before sending toots lacking media descriptions' /> + </LocalSettingsPageItem> + <LocalSettingsPageItem + settings={settings} item={['side_arm']} id='mastodon-settings--side_arm' options={[ @@ -114,6 +123,29 @@ export default class LocalSettingsPage extends React.PureComponent { </section> </div> ), + ({ intl, onChange, settings }) => ( + <div className='glitch local-settings__page content_warnings'> + <h1><FormattedMessage id='settings.content_warnings' defaultMessage='Content warnings' /></h1> + <LocalSettingsPageItem + settings={settings} + item={['content_warnings', 'auto_unfold']} + id='mastodon-settings--content_warnings-auto_unfold' + onChange={onChange} + > + <FormattedMessage id='settings.enable_content_warnings_auto_unfold' defaultMessage='Automatically unfold content-warnings' /> + </LocalSettingsPageItem> + <LocalSettingsPageItem + settings={settings} + item={['content_warnings', 'filter']} + id='mastodon-settings--content_warnings-auto_unfold' + onChange={onChange} + dependsOn={[['content_warnings', 'auto_unfold']]} + placeholder={intl.formatMessage(messages.regexp)} + > + <FormattedMessage id='settings.content_warnings_filter' defaultMessage='Content warnings to not automatically unfold:' /> + </LocalSettingsPageItem> + </div> + ), ({ onChange, settings }) => ( <div className='glitch local-settings__page collapsed'> <h1><FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' /></h1> diff --git a/app/javascript/flavours/glitch/features/local_settings/page/item/index.js b/app/javascript/flavours/glitch/features/local_settings/page/item/index.js index 66e84dfe1..fe237f11e 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/item/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/item/index.js @@ -19,18 +19,20 @@ export default class LocalSettingsPageItem extends React.PureComponent { message: PropTypes.string.isRequired, })), settings: ImmutablePropTypes.map.isRequired, + placeholder: PropTypes.string, }; handleChange = e => { const { target } = e; - const { item, onChange, options } = this.props; + const { item, onChange, options, placeholder } = this.props; if (options && options.length > 0) onChange(item, target.value); + else if (placeholder) onChange(item, target.value); else onChange(item, target.checked); } render () { const { handleChange } = this; - const { settings, item, id, options, children, dependsOn, dependsOnNot } = this.props; + const { settings, item, id, options, children, dependsOn, dependsOnNot, placeholder } = this.props; let enabled = true; if (dependsOn) { @@ -70,6 +72,22 @@ export default class LocalSettingsPageItem extends React.PureComponent { </p> </label> ); + } else if (placeholder) { + return ( + <label className='glitch local-settings__page__item' htmlFor={id}> + <p>{children}</p> + <p> + <input + id={id} + type='text' + value={settings.getIn(item)} + placeholder={placeholder} + onChange={handleChange} + disabled={!enabled} + /> + </p> + </label> + ); } else return ( <label className='glitch local-settings__page__item' htmlFor={id}> <input diff --git a/app/javascript/flavours/glitch/features/mutes/index.js b/app/javascript/flavours/glitch/features/mutes/index.js index 87517eec9..d94c1d8ad 100644 --- a/app/javascript/flavours/glitch/features/mutes/index.js +++ b/app/javascript/flavours/glitch/features/mutes/index.js @@ -42,6 +42,10 @@ export default class Mutes extends ImmutablePureComponent { } } + shouldUpdateScroll = (prevRouterProps, { location }) => { + return !(location.state && location.state.mastodonModalOpen); + } + render () { const { intl, accountIds } = this.props; @@ -56,7 +60,7 @@ export default class Mutes extends ImmutablePureComponent { return ( <Column name='mutes' icon='volume-off' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> - <ScrollContainer scrollKey='mutes'> + <ScrollContainer scrollKey='mutes' shouldUpdateScroll={this.shouldUpdateScroll}> <div className='scrollable mutes' onScroll={this.handleScroll}> {accountIds.map(id => <AccountContainer key={id} id={id} /> diff --git a/app/javascript/flavours/glitch/features/pinned_statuses/index.js b/app/javascript/flavours/glitch/features/pinned_statuses/index.js index e7fa7ac0d..f56d70176 100644 --- a/app/javascript/flavours/glitch/features/pinned_statuses/index.js +++ b/app/javascript/flavours/glitch/features/pinned_statuses/index.js @@ -41,10 +41,6 @@ export default class PinnedStatuses extends ImmutablePureComponent { this.column = c; } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, statusIds, hasMore } = this.props; @@ -54,7 +50,6 @@ export default class PinnedStatuses extends ImmutablePureComponent { <StatusList statusIds={statusIds} scrollKey='pinned_statuses' - shouldUpdateScroll={this.shouldUpdateScroll} hasMore={hasMore} /> </Column> diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.js b/app/javascript/flavours/glitch/features/public_timeline/index.js index 3eb92cafa..a6c0b1688 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/index.js +++ b/app/javascript/flavours/glitch/features/public_timeline/index.js @@ -71,10 +71,6 @@ export default class PublicTimeline extends React.PureComponent { this.props.dispatch(expandPublicTimeline({ maxId })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, columnId, hasUnread, multiColumn } = this.props; const pinned = !!columnId; @@ -99,7 +95,6 @@ export default class PublicTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} trackScroll={!pinned} scrollKey={`public_timeline-${columnId}`} - shouldUpdateScroll={this.shouldUpdateScroll} emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} /> </Column> diff --git a/app/javascript/flavours/glitch/features/reblogs/index.js b/app/javascript/flavours/glitch/features/reblogs/index.js index 25b792b39..c0a65d1de 100644 --- a/app/javascript/flavours/glitch/features/reblogs/index.js +++ b/app/javascript/flavours/glitch/features/reblogs/index.js @@ -33,6 +33,10 @@ export default class Reblogs extends ImmutablePureComponent { } } + shouldUpdateScroll = (prevRouterProps, { location }) => { + return !(location.state && location.state.mastodonModalOpen); + } + render () { const { accountIds } = this.props; @@ -48,7 +52,7 @@ export default class Reblogs extends ImmutablePureComponent { <Column> <ColumnBackButton /> - <ScrollContainer scrollKey='reblogs'> + <ScrollContainer scrollKey='reblogs' shouldUpdateScroll={this.shouldUpdateScroll}> <div className='scrollable reblogs'> {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} </div> diff --git a/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js index 08b9e9e57..c488f9541 100644 --- a/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js +++ b/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js @@ -47,10 +47,6 @@ export default class CommunityTimeline extends React.PureComponent { this.props.dispatch(expandCommunityTimeline({ maxId })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl } = this.props; @@ -66,7 +62,6 @@ export default class CommunityTimeline extends React.PureComponent { timelineId='community' onLoadMore={this.handleLoadMore} scrollKey='standalone_public_timeline' - shouldUpdateScroll={this.shouldUpdateScroll} trackScroll={false} /> </Column> diff --git a/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js index d2b1971ec..dc02f1c91 100644 --- a/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js +++ b/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js @@ -41,10 +41,6 @@ export default class HashtagTimeline extends React.PureComponent { this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { hashtag } = this.props; @@ -59,7 +55,6 @@ export default class HashtagTimeline extends React.PureComponent { <StatusListContainer trackScroll={false} scrollKey='standalone_hashtag_timeline' - shouldUpdateScroll={this.shouldUpdateScroll} timelineId={`hashtag:${hashtag}`} onLoadMore={this.handleLoadMore} /> diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index 99e2c594b..009aa49eb 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -4,7 +4,7 @@ import IconButton from 'flavours/glitch/components/icon_button'; import ImmutablePropTypes from 'react-immutable-proptypes'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import { defineMessages, injectIntl } from 'react-intl'; -import { me } from 'flavours/glitch/util/initial_state'; +import { me, isStaff } from 'flavours/glitch/util/initial_state'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -26,6 +26,8 @@ const messages = defineMessages({ 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' }, }); @injectIntl @@ -70,11 +72,11 @@ export default class ActionBar extends React.PureComponent { } handleDeleteClick = () => { - this.props.onDelete(this.props.status); + this.props.onDelete(this.props.status, this.context.router.history); } handleRedraftClick = () => { - this.props.onDelete(this.props.status, true); + this.props.onDelete(this.props.status, this.context.router.history, true); } handleDirectClick = () => { @@ -146,6 +148,11 @@ export default class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); + 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')}` }); + } } const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && ( diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index ddc2f820a..3d309976a 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -38,6 +38,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { HotKeys } from 'react-hotkeys'; import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen'; +import { autoUnfoldCW } from 'flavours/glitch/util/content_warning'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -82,8 +83,8 @@ export default class Status extends ImmutablePureComponent { state = { fullscreen: false, - isExpanded: false, - threadExpanded: null, + isExpanded: undefined, + threadExpanded: undefined, }; componentWillMount () { @@ -95,9 +96,14 @@ export default class Status extends ImmutablePureComponent { } componentWillReceiveProps (nextProps) { + if (this.state.isExpanded === undefined) { + const isExpanded = autoUnfoldCW(nextProps.settings, nextProps.status); + if (isExpanded !== undefined) this.setState({ isExpanded: isExpanded }); + } if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { this._scrolledIntoView = false; this.props.dispatch(fetchStatus(nextProps.params.statusId)); + this.setState({ isExpanded: autoUnfoldCW(nextProps.settings, nextProps.status) }); } } @@ -159,16 +165,16 @@ export default class Status extends ImmutablePureComponent { } } - handleDeleteClick = (status, withRedraft = false) => { + handleDeleteClick = (status, history, withRedraft = false) => { const { dispatch, intl } = this.props; if (!deleteModal) { - dispatch(deleteStatus(status.get('id'), withRedraft)); + 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), - onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)), + onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), })); } } diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js index ee71e514a..f87c078ec 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js @@ -1,11 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; 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 { Link } from 'react-router-dom'; import BundleContainer from '../containers/bundle_container'; import ColumnLoading from './column_loading'; @@ -29,6 +30,10 @@ const componentMap = { 'LIST': ListTimeline, }; +const messages = defineMessages({ + publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }, +}); + @component => injectIntl(component, { withRef: true }) export default class ColumnsArea extends ImmutablePureComponent { @@ -146,18 +151,26 @@ export default class ColumnsArea extends ImmutablePureComponent { } render () { - const { columns, children, singleColumn } = this.props; + const { columns, children, singleColumn, intl } = this.props; const { shouldAnimate } = this.state; const columnIndex = getIndex(this.context.router.history.location.pathname); this.pendingIndex = null; if (singleColumn) { - return columnIndex !== -1 ? ( - <ReactSwipeableViews index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}> + const floatingActionButton = this.context.router.history.location.pathname === '/statuses/new' ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><i className='fa fa-pencil' /></Link>; + + return 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> - ) : <div className='columns-area'>{children}</div>; + </ReactSwipeableViews>, + + floatingActionButton, + ] : [ + <div className='columns-area'>{children}</div>, + + floatingActionButton, + ]; } return ( diff --git a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js index 89b455dd8..b2fee21e1 100644 --- a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js +++ b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js @@ -1,19 +1,19 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { NavLink } from 'react-router-dom'; +import { NavLink, withRouter } from 'react-router-dom'; import { FormattedMessage, injectIntl } from 'react-intl'; import { debounce } from 'lodash'; import { isUserTouching } from 'flavours/glitch/util/is_mobile'; export const links = [ - <NavLink className='tabs-bar__link primary' to='/statuses/new' data-preview-title-id='tabs_bar.compose' data-preview-icon='pencil' ><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>, <NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><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' ><i className='fa fa-fw fa-bell' /><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' ><i className='fa fa-fw fa-users' /><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' ><i className='fa fa-fw fa-globe' /><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' ><i className='fa fa-fw fa-search' /><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='asterisk' ><i className='fa fa-fw fa-asterisk' /></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' ><i className='fa fa-fw fa-bars' /></NavLink>, ]; export function getIndex (path) { @@ -25,14 +25,12 @@ export function getLink (index) { } @injectIntl -export default class TabsBar extends React.Component { - - static contextTypes = { - router: PropTypes.object.isRequired, - } +@withRouter +export default class TabsBar extends React.PureComponent { static propTypes = { intl: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, } setRef = ref => { @@ -60,7 +58,7 @@ export default class TabsBar extends React.Component { const listener = debounce(() => { nextTab.removeEventListener('transitionend', listener); - this.context.router.history.push(to); + this.props.history.push(to); }, 50); nextTab.addEventListener('transitionend', listener); diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index d58e11b55..1cff94321 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -437,6 +437,8 @@ export default class UI extends React.Component { <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} /> <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> + <WrappedRoute path='/search' component={Drawer} content={children} componentParams={{ isSearchPage: true }} /> + <WrappedRoute path='/statuses/new' component={Drawer} content={children} /> <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} /> diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index 1d24f0e9a..063ae3943 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -13,6 +13,11 @@ const initialState = ImmutableMap({ side_arm_reply_mode : 'keep', show_reply_count : false, always_show_spoilers_field: false, + confirm_missing_media_description: false, + content_warnings : ImmutableMap({ + auto_unfold : false, + filter : null, + }), collapsed : ImmutableMap({ enabled : true, auto : ImmutableMap({ diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index c61cd038f..86c77f980 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -503,3 +503,27 @@ margin-left: 5px; } } + +.floating-action-button { + position: fixed; + display: flex; + justify-content: center; + align-items: center; + width: 3.9375rem; + height: 3.9375rem; + bottom: 1.3125rem; + right: 1.3125rem; + background: darken($ui-highlight-color, 3%); + color: $white; + border-radius: 50%; + font-size: 21px; + line-height: 21px; + text-decoration: none; + box-shadow: 2px 3px 9px rgba($base-shadow-color, 0.4); + + &:hover, + &:focus, + &:active { + background: lighten($ui-highlight-color, 7%); + } +} diff --git a/app/javascript/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss index 0432b233a..6a9af4490 100644 --- a/app/javascript/flavours/glitch/styles/components/drawer.scss +++ b/app/javascript/flavours/glitch/styles/components/drawer.scss @@ -5,7 +5,6 @@ padding: 10px 5px; width: 300px; flex: none; - contain: strict; &:first-child { padding-left: 10px; @@ -49,7 +48,6 @@ background: lighten($ui-base-color, 13%); overflow-x: hidden; overflow-y: auto; - contain: strict; & > .mastodon { flex: 1; @@ -253,7 +251,6 @@ background: $ui-base-color; overflow-x: hidden; overflow-y: auto; - contain: strict; & > header { border-bottom: 1px solid darken($ui-base-color, 4%); diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index b5d79f4d7..17901f233 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -347,6 +347,23 @@ margin-bottom: 10px; box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + &.inactive { + opacity: 0.5; + + .public-account-header__image, + .avatar { + filter: grayscale(100%); + } + + .logo-button { + background-color: $secondary-text-color; + + svg path:last-child { + fill: $secondary-text-color; + } + } + } + &__image { border-radius: 4px 4px 0 0; overflow: hidden; @@ -588,6 +605,10 @@ border-bottom: 4px solid $highlight-text-color; opacity: 1; } + + &.inactive::after { + border-bottom-color: $secondary-text-color; + } } &:hover { diff --git a/app/javascript/flavours/glitch/util/content_warning.js b/app/javascript/flavours/glitch/util/content_warning.js new file mode 100644 index 000000000..29e221c8e --- /dev/null +++ b/app/javascript/flavours/glitch/util/content_warning.js @@ -0,0 +1,19 @@ +export function autoUnfoldCW (settings, status) { + if (!settings.getIn(['content_warnings', 'auto_unfold'])) { + return false; + } + + const rawRegex = settings.getIn(['content_warnings', 'filter']); + let regex = null; + + try { + regex = rawRegex && new RegExp(rawRegex.trim(), 'i'); + } catch (e) { + // Bad regex, don't affect filters + } + + if (!(status && regex)) { + return undefined; + } + return !regex.test(status.get('spoiler_text')); +} diff --git a/app/javascript/flavours/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js index c6416db2d..82a1ef89c 100644 --- a/app/javascript/flavours/glitch/util/emoji/index.js +++ b/app/javascript/flavours/glitch/util/emoji/index.js @@ -62,6 +62,10 @@ const emojify = (str, customEmojis = {}) => { const title = shortCode ? `:${shortCode}:` : ''; replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${filename}.svg" />`; rend = i + match.length; + // If the matched character was followed by VS15 (for selecting text presentation), skip it. + if (str.codePointAt(rend) === 65038) { + rend += 1; + } } rtn += str.slice(0, i) + replacement; str = str.slice(rend); diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js index 2c4ab9091..fdf004527 100644 --- a/app/javascript/flavours/glitch/util/initial_state.js +++ b/app/javascript/flavours/glitch/util/initial_state.js @@ -19,6 +19,8 @@ export const boostModal = getMeta('boost_modal'); export const favouriteModal = getMeta('favourite_modal'); export const deleteModal = getMeta('delete_modal'); export const me = getMeta('me'); +export const searchEnabled = getMeta('search_enabled'); export const maxChars = (initialState && initialState.max_toot_chars) || 500; +export const isStaff = getMeta('is_staff'); export default initialState; |