diff options
Diffstat (limited to 'app/javascript/flavours')
130 files changed, 2934 insertions, 3334 deletions
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 02aa4f144..15476fc59 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -1,19 +1,21 @@ -import api from '../api'; -import { CancelToken, isCancel } from 'axios'; +import axios from 'axios'; import { throttle } from 'lodash'; +import { defineMessages } from 'react-intl'; +import api from 'flavours/glitch/api'; import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light'; -import { useEmoji } from './emojis'; -import { tagHistory } from '../settings'; +import { tagHistory } from 'flavours/glitch/settings'; import { recoverHashtags } from 'flavours/glitch/utils/hashtag'; import resizeImage from 'flavours/glitch/utils/resize_image'; +import { showAlert, showAlertForError } from './alerts'; +import { useEmoji } from './emojis'; import { importFetchedAccounts } from './importer'; -import { updateTimeline } from './timelines'; -import { showAlertForError } from './alerts'; -import { showAlert } from './alerts'; import { openModal } from './modal'; -import { defineMessages } from 'react-intl'; +import { updateTimeline } from './timelines'; -let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags; +/** @type {AbortController | undefined} */ +let fetchComposeSuggestionsAccountsController; +/** @type {AbortController | undefined} */ +let fetchComposeSuggestionsTagsController; export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; export const COMPOSE_CYCLE_ELEFRIEND = 'COMPOSE_CYCLE_ELEFRIEND'; @@ -25,11 +27,13 @@ export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; export const COMPOSE_DIRECT = 'COMPOSE_DIRECT'; export const COMPOSE_MENTION = 'COMPOSE_MENTION'; export const COMPOSE_RESET = 'COMPOSE_RESET'; -export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; -export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS'; -export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; -export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS'; -export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; + +export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; +export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS'; +export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; +export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS'; +export const COMPOSE_UPLOAD_PROCESSING = 'COMPOSE_UPLOAD_PROCESSING'; +export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; export const THUMBNAIL_UPLOAD_REQUEST = 'THUMBNAIL_UPLOAD_REQUEST'; export const THUMBNAIL_UPLOAD_SUCCESS = 'THUMBNAIL_UPLOAD_SUCCESS'; @@ -83,10 +87,8 @@ const messages = defineMessages({ uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, }); -const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1); - export const ensureComposeIsVisible = (getState, routerHistory) => { - if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) { + if (!getState().getIn(['compose', 'mounted'])) { routerHistory.push('/publish'); } }; @@ -307,13 +309,16 @@ export function uploadCompose(files) { if (status === 200) { dispatch(uploadComposeSuccess(data, f)); } else if (status === 202) { + dispatch(uploadComposeProcessing()); + let tryCount = 1; + const poll = () => { api(getState).get(`/api/v1/media/${data.id}`).then(response => { if (response.status === 200) { dispatch(uploadComposeSuccess(response.data, f)); } else if (response.status === 206) { - let retryAfter = (Math.log2(tryCount) || 1) * 1000; + const retryAfter = (Math.log2(tryCount) || 1) * 1000; tryCount += 1; setTimeout(() => poll(), retryAfter); } @@ -328,6 +333,10 @@ export function uploadCompose(files) { }; }; +export const uploadComposeProcessing = () => ({ + type: COMPOSE_UPLOAD_PROCESSING, +}); + export const uploadThumbnail = (id, file) => (dispatch, getState) => { dispatch(uploadThumbnailRequest()); @@ -472,8 +481,8 @@ export function undoUploadCompose(media_id) { }; export function clearComposeSuggestions() { - if (cancelFetchComposeSuggestionsAccounts) { - cancelFetchComposeSuggestionsAccounts(); + if (fetchComposeSuggestionsAccountsController) { + fetchComposeSuggestionsAccountsController.abort(); } return { type: COMPOSE_SUGGESTIONS_CLEAR, @@ -481,14 +490,14 @@ export function clearComposeSuggestions() { }; const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => { - if (cancelFetchComposeSuggestionsAccounts) { - cancelFetchComposeSuggestionsAccounts(); + if (fetchComposeSuggestionsAccountsController) { + fetchComposeSuggestionsAccountsController.abort(); } + fetchComposeSuggestionsAccountsController = new AbortController(); + api(getState).get('/api/v1/accounts/search', { - cancelToken: new CancelToken(cancel => { - cancelFetchComposeSuggestionsAccounts = cancel; - }), + signal: fetchComposeSuggestionsAccountsController.signal, params: { q: token.slice(1), @@ -499,9 +508,11 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => dispatch(importFetchedAccounts(response.data)); dispatch(readyComposeSuggestionsAccounts(token, response.data)); }).catch(error => { - if (!isCancel(error)) { + if (!axios.isCancel(error)) { dispatch(showAlertForError(error)); } + }).finally(() => { + fetchComposeSuggestionsAccountsController = undefined; }); }, 200, { leading: true, trailing: true }); @@ -511,16 +522,16 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => { }; const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => { - if (cancelFetchComposeSuggestionsTags) { - cancelFetchComposeSuggestionsTags(); + if (fetchComposeSuggestionsTagsController) { + fetchComposeSuggestionsTagsController.abort(); } dispatch(updateSuggestionTags(token)); + fetchComposeSuggestionsTagsController = new AbortController(); + api(getState).get('/api/v2/search', { - cancelToken: new CancelToken(cancel => { - cancelFetchComposeSuggestionsTags = cancel; - }), + signal: fetchComposeSuggestionsTagsController.signal, params: { type: 'hashtags', @@ -531,9 +542,11 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => { }).then(({ data }) => { dispatch(readyComposeSuggestionsTags(token, data.hashtags)); }).catch(error => { - if (!isCancel(error)) { + if (!axios.isCancel(error)) { dispatch(showAlertForError(error)); } + }).finally(() => { + fetchComposeSuggestionsTagsController = undefined; }); }, 200, { leading: true, trailing: true }); diff --git a/app/javascript/flavours/glitch/actions/featured_tags.js b/app/javascript/flavours/glitch/actions/featured_tags.js new file mode 100644 index 000000000..18bb61539 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/featured_tags.js @@ -0,0 +1,34 @@ +import api from '../api'; + +export const FEATURED_TAGS_FETCH_REQUEST = 'FEATURED_TAGS_FETCH_REQUEST'; +export const FEATURED_TAGS_FETCH_SUCCESS = 'FEATURED_TAGS_FETCH_SUCCESS'; +export const FEATURED_TAGS_FETCH_FAIL = 'FEATURED_TAGS_FETCH_FAIL'; + +export const fetchFeaturedTags = (id) => (dispatch, getState) => { + if (getState().getIn(['user_lists', 'featured_tags', id, 'items'])) { + return; + } + + dispatch(fetchFeaturedTagsRequest(id)); + + api(getState).get(`/api/v1/accounts/${id}/featured_tags`) + .then(({ data }) => dispatch(fetchFeaturedTagsSuccess(id, data))) + .catch(err => dispatch(fetchFeaturedTagsFail(id, err))); +}; + +export const fetchFeaturedTagsRequest = (id) => ({ + type: FEATURED_TAGS_FETCH_REQUEST, + id, +}); + +export const fetchFeaturedTagsSuccess = (id, tags) => ({ + type: FEATURED_TAGS_FETCH_SUCCESS, + id, + tags, +}); + +export const fetchFeaturedTagsFail = (id, error) => ({ + type: FEATURED_TAGS_FETCH_FAIL, + id, + error, +}); diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js index 776782693..f21c0058b 100644 --- a/app/javascript/flavours/glitch/actions/search.js +++ b/app/javascript/flavours/glitch/actions/search.js @@ -29,7 +29,8 @@ export function clearSearch() { export function submitSearch() { return (dispatch, getState) => { - const value = getState().getIn(['search', 'value']); + const value = getState().getIn(['search', 'value']); + const signedIn = !!getState().getIn(['meta', 'me']); if (value.length === 0) { dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '')); @@ -41,7 +42,7 @@ export function submitSearch() { api(getState).get('/api/v2/search', { params: { q: value, - resolve: true, + resolve: signedIn, limit: 10, }, }).then(response => { diff --git a/app/javascript/flavours/glitch/actions/server.js b/app/javascript/flavours/glitch/actions/server.js index af8fef780..31d4aea10 100644 --- a/app/javascript/flavours/glitch/actions/server.js +++ b/app/javascript/flavours/glitch/actions/server.js @@ -5,6 +5,14 @@ export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST'; export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS'; export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL'; +export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST'; +export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS'; +export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL'; + +export const SERVER_DOMAIN_BLOCKS_FETCH_REQUEST = 'SERVER_DOMAIN_BLOCKS_FETCH_REQUEST'; +export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS'; +export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL'; + export const fetchServer = () => (dispatch, getState) => { dispatch(fetchServerRequest()); @@ -28,3 +36,56 @@ const fetchServerFail = error => ({ type: SERVER_FETCH_FAIL, error, }); + +export const fetchExtendedDescription = () => (dispatch, getState) => { + dispatch(fetchExtendedDescriptionRequest()); + + api(getState) + .get('/api/v1/instance/extended_description') + .then(({ data }) => dispatch(fetchExtendedDescriptionSuccess(data))) + .catch(err => dispatch(fetchExtendedDescriptionFail(err))); +}; + +const fetchExtendedDescriptionRequest = () => ({ + type: EXTENDED_DESCRIPTION_REQUEST, +}); + +const fetchExtendedDescriptionSuccess = description => ({ + type: EXTENDED_DESCRIPTION_SUCCESS, + description, +}); + +const fetchExtendedDescriptionFail = error => ({ + type: EXTENDED_DESCRIPTION_FAIL, + error, +}); + +export const fetchDomainBlocks = () => (dispatch, getState) => { + dispatch(fetchDomainBlocksRequest()); + + api(getState) + .get('/api/v1/instance/domain_blocks') + .then(({ data }) => dispatch(fetchDomainBlocksSuccess(true, data))) + .catch(err => { + if (err.response.status === 404) { + dispatch(fetchDomainBlocksSuccess(false, [])); + } else { + dispatch(fetchDomainBlocksFail(err)); + } + }); +}; + +const fetchDomainBlocksRequest = () => ({ + type: SERVER_DOMAIN_BLOCKS_FETCH_REQUEST, +}); + +const fetchDomainBlocksSuccess = (isAvailable, blocks) => ({ + type: SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS, + isAvailable, + blocks, +}); + +const fetchDomainBlocksFail = error => ({ + type: SERVER_DOMAIN_BLOCKS_FETCH_FAIL, + error, +}); diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index ef1e4dbbb..a1c4dd43a 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -156,8 +156,8 @@ export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => ex export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); -export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); -export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); +export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, tagged, max_id: maxId }); +export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => { diff --git a/app/javascript/flavours/glitch/api.js b/app/javascript/flavours/glitch/api.js index 645ef6500..6bbddbef6 100644 --- a/app/javascript/flavours/glitch/api.js +++ b/app/javascript/flavours/glitch/api.js @@ -1,20 +1,31 @@ +// @ts-check + import axios from 'axios'; import LinkHeader from 'http-link-header'; import ready from './ready'; +/** + * @param {import('axios').AxiosResponse} response + * @returns {LinkHeader} + */ export const getLinks = response => { const value = response.headers.link; if (!value) { - return { refs: [] }; + return new LinkHeader(); } return LinkHeader.parse(value); }; +/** @type {import('axios').RawAxiosRequestHeaders} */ const csrfHeader = {}; +/** + * @returns {void} + */ const setCSRFHeader = () => { + /** @type {HTMLMetaElement | null} */ const csrfToken = document.querySelector('meta[name=csrf-token]'); if (csrfToken) { @@ -24,6 +35,10 @@ const setCSRFHeader = () => { ready(setCSRFHeader); +/** + * @param {() => import('immutable').Map} getState + * @returns {import('axios').RawAxiosRequestHeaders} + */ const authorizationHeaderFromState = getState => { const accessToken = getState && getState().getIn(['meta', 'access_token'], ''); @@ -36,17 +51,25 @@ const authorizationHeaderFromState = getState => { }; }; -export default getState => axios.create({ - headers: { - ...csrfHeader, - ...authorizationHeaderFromState(getState), - }, - - transformResponse: [function (data) { - try { - return JSON.parse(data); - } catch(Exception) { - return data; - } - }], -}); +/** + * @param {() => import('immutable').Map} getState + * @returns {import('axios').AxiosInstance} + */ +export default function api(getState) { + return axios.create({ + headers: { + ...csrfHeader, + ...authorizationHeaderFromState(getState), + }, + + transformResponse: [ + function (data) { + try { + return JSON.parse(data); + } catch { + return data; + } + }, + ], + }); +} diff --git a/app/javascript/flavours/glitch/components/avatar.js b/app/javascript/flavours/glitch/components/avatar.js index ce91d401d..38fd99af5 100644 --- a/app/javascript/flavours/glitch/components/avatar.js +++ b/app/javascript/flavours/glitch/components/avatar.js @@ -70,6 +70,8 @@ export default class Avatar extends React.PureComponent { onMouseLeave={this.handleMouseLeave} style={style} data-avatar-of={account && `@${account.get('acct')}`} + role='img' + aria-label={account?.get('acct')} /> ); } diff --git a/app/javascript/flavours/glitch/components/dismissable_banner.js b/app/javascript/flavours/glitch/components/dismissable_banner.js new file mode 100644 index 000000000..ff52a619d --- /dev/null +++ b/app/javascript/flavours/glitch/components/dismissable_banner.js @@ -0,0 +1,51 @@ +import React from 'react'; +import IconButton from './icon_button'; +import PropTypes from 'prop-types'; +import { injectIntl, defineMessages } from 'react-intl'; +import { bannerSettings } from 'flavours/glitch/settings'; + +const messages = defineMessages({ + dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' }, +}); + +export default @injectIntl +class DismissableBanner extends React.PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + children: PropTypes.node, + intl: PropTypes.object.isRequired, + }; + + state = { + visible: !bannerSettings.get(this.props.id), + }; + + handleDismiss = () => { + const { id } = this.props; + this.setState({ visible: false }, () => bannerSettings.set(id, true)); + } + + render () { + const { visible } = this.state; + + if (!visible) { + return null; + } + + const { children, intl } = this.props; + + return ( + <div className='dismissable-banner'> + <div className='dismissable-banner__message'> + {children} + </div> + + <div className='dismissable-banner__action'> + <IconButton icon='times' title={intl.formatMessage(messages.dismiss)} onClick={this.handleDismiss} /> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/error_boundary.js b/app/javascript/flavours/glitch/components/error_boundary.js index fd3659de7..e0ca3e2b0 100644 --- a/app/javascript/flavours/glitch/components/error_boundary.js +++ b/app/javascript/flavours/glitch/components/error_boundary.js @@ -4,6 +4,7 @@ import { FormattedMessage } from 'react-intl'; import { source_url } from 'flavours/glitch/initial_state'; import { preferencesLink } from 'flavours/glitch/utils/backend_links'; import StackTrace from 'stacktrace-js'; +import { Helmet } from 'react-helmet'; export default class ErrorBoundary extends React.PureComponent { @@ -122,6 +123,10 @@ export default class ErrorBoundary extends React.PureComponent { )} </ul> </div> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> </div> ); } diff --git a/app/javascript/flavours/glitch/components/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.js index 2bb7f02f7..422b9a8fa 100644 --- a/app/javascript/flavours/glitch/components/hashtag.js +++ b/app/javascript/flavours/glitch/components/hashtag.js @@ -1,7 +1,7 @@ // @ts-check import React from 'react'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; -import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Permalink from './permalink'; @@ -9,10 +9,6 @@ import ShortNumber from 'flavours/glitch/components/short_number'; import Skeleton from 'flavours/glitch/components/skeleton'; import classNames from 'classnames'; -const messages = defineMessages({ - totalVolume: { id: 'hashtag.total_volume', defaultMessage: 'Total volume in the last {days, plural, one {day} other {{days} days}}' }, -}); - class SilentErrorBoundary extends React.Component { static propTypes = { @@ -60,7 +56,6 @@ export const ImmutableHashtag = ({ hashtag }) => ( href={hashtag.get('url')} to={`/tags/${hashtag.get('name')}`} people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1} - uses={hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1} history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()} /> ); @@ -69,39 +64,52 @@ ImmutableHashtag.propTypes = { hashtag: ImmutablePropTypes.map.isRequired, }; -const Hashtag = injectIntl(({ name, href, to, people, uses, history, className, intl }) => ( +const Hashtag = ({ name, href, to, people, uses, history, className, description, withGraph }) => ( <div className={classNames('trends__item', className)}> <div className='trends__item__name'> <Permalink href={href} to={to}> {name ? <React.Fragment>#<span>{name}</span></React.Fragment> : <Skeleton width={50} />} </Permalink> - {typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />} + {description ? ( + <span>{description}</span> + ) : ( + typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} /> + )} </div> - <abbr className='trends__item__current' title={intl.formatMessage(messages.totalVolume, { days: 2 })}> - {typeof uses !== 'undefined' ? <ShortNumber value={uses} /> : <Skeleton width={42} height={36} />} - <span className='trends__item__current__asterisk'>*</span> - </abbr> - - <div className='trends__item__sparkline'> - <SilentErrorBoundary> - <Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}> - <SparklinesCurve style={{ fill: 'none' }} /> - </Sparklines> - </SilentErrorBoundary> - </div> + {typeof uses !== 'undefined' && ( + <div className='trends__item__current'> + <ShortNumber value={uses} /> + </div> + )} + + {withGraph && ( + <div className='trends__item__sparkline'> + <SilentErrorBoundary> + <Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}> + <SparklinesCurve style={{ fill: 'none' }} /> + </Sparklines> + </SilentErrorBoundary> + </div> + )} </div> -)); +); Hashtag.propTypes = { name: PropTypes.string, href: PropTypes.string, to: PropTypes.string, people: PropTypes.number, + description: PropTypes.node, uses: PropTypes.number, history: PropTypes.arrayOf(PropTypes.number), className: PropTypes.string, + withGraph: PropTypes.bool, +}; + +Hashtag.defaultProps = { + withGraph: true, }; export default Hashtag; diff --git a/app/javascript/flavours/glitch/components/image.js b/app/javascript/flavours/glitch/components/image.js new file mode 100644 index 000000000..6e81ddf08 --- /dev/null +++ b/app/javascript/flavours/glitch/components/image.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Blurhash from './blurhash'; +import classNames from 'classnames'; + +export default class Image extends React.PureComponent { + + static propTypes = { + src: PropTypes.string, + srcSet: PropTypes.string, + blurhash: PropTypes.string, + className: PropTypes.string, + }; + + state = { + loaded: false, + }; + + handleLoad = () => this.setState({ loaded: true }); + + render () { + const { src, srcSet, blurhash, className } = this.props; + const { loaded } = this.state; + + return ( + <div className={classNames('image', { loaded }, className)} role='presentation'> + {blurhash && <Blurhash hash={blurhash} className='image__preview' />} + <img src={src} srcSet={srcSet} alt='' onLoad={this.handleLoad} /> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/logo.js b/app/javascript/flavours/glitch/components/logo.js index 3570b3644..ee5c22496 100644 --- a/app/javascript/flavours/glitch/components/logo.js +++ b/app/javascript/flavours/glitch/components/logo.js @@ -1,7 +1,8 @@ import React from 'react'; const Logo = () => ( - <svg viewBox='0 0 261 66' className='logo'> + <svg viewBox='0 0 261 66' className='logo' role='img'> + <title>Mastodon</title> <use xlinkHref='#logo-symbol-wordmark' /> </svg> ); diff --git a/app/javascript/flavours/glitch/components/missing_indicator.js b/app/javascript/flavours/glitch/components/missing_indicator.js index ee5bf7c1e..08e39c236 100644 --- a/app/javascript/flavours/glitch/components/missing_indicator.js +++ b/app/javascript/flavours/glitch/components/missing_indicator.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import illustration from 'flavours/glitch/images/elephant_ui_disappointed.svg'; import classNames from 'classnames'; +import { Helmet } from 'react-helmet'; const MissingIndicator = ({ fullPage }) => ( <div className={classNames('regeneration-indicator', { 'regeneration-indicator--without-header': fullPage })}> @@ -14,6 +15,10 @@ const MissingIndicator = ({ fullPage }) => ( <FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' /> <FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' /> </div> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> </div> ); diff --git a/app/javascript/flavours/glitch/components/navigation_portal.js b/app/javascript/flavours/glitch/components/navigation_portal.js new file mode 100644 index 000000000..16a8ca3f5 --- /dev/null +++ b/app/javascript/flavours/glitch/components/navigation_portal.js @@ -0,0 +1,30 @@ +import React from 'react'; +import { Switch, Route, withRouter } from 'react-router-dom'; +import { showTrends } from 'flavours/glitch/initial_state'; +import Trends from 'flavours/glitch/features/getting_started/containers/trends_container'; +import AccountNavigation from 'flavours/glitch/features/account/navigation'; + +const DefaultNavigation = () => ( + <> + {showTrends && ( + <> + <div className='flex-spacer' /> + <Trends /> + </> + )} + </> +); + +export default @withRouter +class NavigationPortal extends React.PureComponent { + + render () { + return ( + <Switch> + <Route path='/@:acct/(tagged/:tagged?)?' component={AccountNavigation} /> + <Route component={DefaultNavigation} /> + </Switch> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/server_banner.js b/app/javascript/flavours/glitch/components/server_banner.js index 16360ec56..9cb0ef13c 100644 --- a/app/javascript/flavours/glitch/components/server_banner.js +++ b/app/javascript/flavours/glitch/components/server_banner.js @@ -1,19 +1,21 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import { domain } from 'flavours/glitch/initial_state'; -import { fetchServer } from 'flavours/glitch/actions/server'; +import React from 'react'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; -import Account from 'flavours/glitch/containers/account_container'; +import { fetchServer } from 'flavours/glitch/actions/server'; import ShortNumber from 'flavours/glitch/components/short_number'; import Skeleton from 'flavours/glitch/components/skeleton'; -import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; +import Account from 'flavours/glitch/containers/account_container'; +import { domain } from 'flavours/glitch/initial_state'; +import Image from 'flavours/glitch/components/image'; +import { Link } from 'react-router-dom'; const messages = defineMessages({ aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' }, }); const mapStateToProps = state => ({ - server: state.get('server'), + server: state.getIn(['server', 'server']), }); export default @connect(mapStateToProps) @@ -41,7 +43,7 @@ class ServerBanner extends React.PureComponent { <FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} /> </div> - <img src={server.get('thumbnail')} alt={server.get('title')} className='server-banner__hero' /> + <Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' /> <div className='server-banner__description'> {isLoading ? ( @@ -83,7 +85,7 @@ class ServerBanner extends React.PureComponent { <hr className='spacer' /> - <a className='button button--block button-secondary' href='/about/more' target='_blank'><FormattedMessage id='server_banner.learn_more' defaultMessage='Learn more' /></a> + <Link className='button button--block button-secondary' to='/about'><FormattedMessage id='server_banner.learn_more' defaultMessage='Learn more' /></Link> </div> ); } diff --git a/app/javascript/flavours/glitch/components/skeleton.js b/app/javascript/flavours/glitch/components/skeleton.js index 09093e99c..6a17ffb26 100644 --- a/app/javascript/flavours/glitch/components/skeleton.js +++ b/app/javascript/flavours/glitch/components/skeleton.js @@ -4,8 +4,8 @@ import PropTypes from 'prop-types'; const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>‌</span>; Skeleton.propTypes = { - width: PropTypes.number, - height: PropTypes.number, + width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), }; export default Skeleton; diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index deb9cfc15..b260b5bca 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -242,7 +242,7 @@ class StatusActionBar extends ImmutablePureComponent { } if (writtenByMe) { - // menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); + menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); } else { diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index fe0d4c043..61788f350 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -124,6 +124,9 @@ export default class StatusContent extends React.PureComponent { link.setAttribute('title', link.href); link.classList.add('unhandled-link'); + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener nofollow noreferrer'); + try { if (tagLinks && isLinkMisleading(link)) { // Add a tag besides the link to display its origin @@ -149,9 +152,6 @@ export default class StatusContent extends React.PureComponent { if (tagLinks && e instanceof TypeError) link.removeAttribute('href'); } } - - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener noreferrer'); } } diff --git a/app/javascript/flavours/glitch/containers/mastodon.js b/app/javascript/flavours/glitch/containers/mastodon.js index bc0311293..cf870102b 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.js +++ b/app/javascript/flavours/glitch/containers/mastodon.js @@ -1,22 +1,25 @@ -import React from 'react'; -import { Provider } from 'react-redux'; import PropTypes from 'prop-types'; -import configureStore from 'flavours/glitch/store/configureStore'; +import React from 'react'; +import { Helmet } from 'react-helmet'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter, Route } from 'react-router-dom'; import { ScrollContext } from 'react-router-scroll-4'; +import configureStore from 'flavours/glitch/store/configureStore'; import UI from 'flavours/glitch/features/ui'; import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; import { hydrateStore } from 'flavours/glitch/actions/store'; -import { connectUserStream } from 'flavours/glitch/actions/streaming'; import { checkDeprecatedLocalSettings } from 'flavours/glitch/actions/local_settings'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from 'locales'; -import initialState from 'flavours/glitch/initial_state'; +import { connectUserStream } from 'flavours/glitch/actions/streaming'; import ErrorBoundary from 'flavours/glitch/components/error_boundary'; +import initialState, { title as siteTitle } from 'flavours/glitch/initial_state'; +import { getLocale } from 'locales'; const { localeData, messages } = getLocale(); addLocaleData(localeData); +const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`; + export const store = configureStore(); const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); @@ -78,15 +81,17 @@ export default class Mastodon extends React.PureComponent { return ( <IntlProvider locale={locale} messages={messages}> - <Provider store={store}> + <ReduxProvider store={store}> <ErrorBoundary> - <BrowserRouter basename='/web'> + <BrowserRouter> <ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}> <Route path='/' component={UI} /> </ScrollContext> </BrowserRouter> + + <Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} /> </ErrorBoundary> - </Provider> + </ReduxProvider> </IntlProvider> ); } diff --git a/app/javascript/flavours/glitch/extra_polyfills.js b/app/javascript/flavours/glitch/extra_polyfills.js index 3acc55abd..0d45c23b0 100644 --- a/app/javascript/flavours/glitch/extra_polyfills.js +++ b/app/javascript/flavours/glitch/extra_polyfills.js @@ -1,3 +1,4 @@ +import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'; import 'intersection-observer'; import 'requestidlecallback'; import objectFitImages from 'object-fit-images'; diff --git a/app/javascript/flavours/glitch/features/about/index.js b/app/javascript/flavours/glitch/features/about/index.js new file mode 100644 index 000000000..3d26c59bc --- /dev/null +++ b/app/javascript/flavours/glitch/features/about/index.js @@ -0,0 +1,222 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import Column from 'flavours/glitch/components/column'; +import LinkFooter from 'flavours/glitch/features/ui/components/link_footer'; +import { Helmet } from 'react-helmet'; +import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server'; +import Account from 'flavours/glitch/containers/account_container'; +import Skeleton from 'flavours/glitch/components/skeleton'; +import Icon from 'flavours/glitch/components/icon'; +import classNames from 'classnames'; +import Image from 'flavours/glitch/components/image'; + +const messages = defineMessages({ + title: { id: 'column.about', defaultMessage: 'About' }, + rules: { id: 'about.rules', defaultMessage: 'Server rules' }, + blocks: { id: 'about.blocks', defaultMessage: 'Moderated servers' }, + silenced: { id: 'about.domain_blocks.silenced.title', defaultMessage: 'Limited' }, + silencedExplanation: { id: 'about.domain_blocks.silenced.explanation', defaultMessage: 'You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.' }, + suspended: { id: 'about.domain_blocks.suspended.title', defaultMessage: 'Suspended' }, + suspendedExplanation: { id: 'about.domain_blocks.suspended.explanation', defaultMessage: 'No data from this server will be processed, stored or exchanged, making any interaction or communication with users from this server impossible.' }, +}); + +const severityMessages = { + silence: { + title: messages.silenced, + explanation: messages.silencedExplanation, + }, + + suspend: { + title: messages.suspended, + explanation: messages.suspendedExplanation, + }, +}; + +const mapStateToProps = state => ({ + server: state.getIn(['server', 'server']), + extendedDescription: state.getIn(['server', 'extendedDescription']), + domainBlocks: state.getIn(['server', 'domainBlocks']), +}); + +class Section extends React.PureComponent { + + static propTypes = { + title: PropTypes.string, + children: PropTypes.node, + open: PropTypes.bool, + onOpen: PropTypes.func, + }; + + state = { + collapsed: !this.props.open, + }; + + handleClick = () => { + const { onOpen } = this.props; + const { collapsed } = this.state; + + this.setState({ collapsed: !collapsed }, () => onOpen && onOpen()); + } + + render () { + const { title, children } = this.props; + const { collapsed } = this.state; + + return ( + <div className={classNames('about__section', { active: !collapsed })}> + <div className='about__section__title' role='button' tabIndex='0' onClick={this.handleClick}> + <Icon id={collapsed ? 'chevron-right' : 'chevron-down'} fixedWidth /> {title} + </div> + + {!collapsed && ( + <div className='about__section__body'>{children}</div> + )} + </div> + ); + } + +} + +export default @connect(mapStateToProps) +@injectIntl +class About extends React.PureComponent { + + static propTypes = { + server: ImmutablePropTypes.map, + extendedDescription: ImmutablePropTypes.map, + domainBlocks: ImmutablePropTypes.contains({ + isLoading: PropTypes.bool, + isAvailable: PropTypes.bool, + items: ImmutablePropTypes.list, + }), + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, + }; + + componentDidMount () { + const { dispatch } = this.props; + dispatch(fetchServer()); + dispatch(fetchExtendedDescription()); + } + + handleDomainBlocksOpen = () => { + const { dispatch } = this.props; + dispatch(fetchDomainBlocks()); + } + + render () { + const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props; + const isLoading = server.get('isLoading'); + + return ( + <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}> + <div className='scrollable about'> + <div className='about__header'> + <Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' /> + <h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1> + <p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank'>Mastodon</a> }} /></p> + </div> + + <div className='about__meta'> + <div className='about__meta__column'> + <h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4> + + <Account id={server.getIn(['contact', 'account', 'id'])} /> + </div> + + <hr className='about__meta__divider' /> + + <div className='about__meta__column'> + <h4><FormattedMessage id='about.contact' defaultMessage='Contact:' /></h4> + + {isLoading ? <Skeleton width='10ch' /> : <a className='about__mail' href={`mailto:${server.getIn(['contact', 'email'])}`}>{server.getIn(['contact', 'email'])}</a>} + </div> + </div> + + <Section open title={intl.formatMessage(messages.title)}> + {extendedDescription.get('isLoading') ? ( + <> + <Skeleton width='100%' /> + <br /> + <Skeleton width='100%' /> + <br /> + <Skeleton width='100%' /> + <br /> + <Skeleton width='70%' /> + </> + ) : (extendedDescription.get('content')?.length > 0 ? ( + <div + className='prose' + dangerouslySetInnerHTML={{ __html: extendedDescription.get('content') }} + /> + ) : ( + <p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p> + ))} + </Section> + + <Section title={intl.formatMessage(messages.rules)}> + {!isLoading && (server.get('rules').isEmpty() ? ( + <p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p> + ) : ( + <ol className='rules-list'> + {server.get('rules').map(rule => ( + <li key={rule.get('id')}> + <span className='rules-list__text'>{rule.get('text')}</span> + </li> + ))} + </ol> + ))} + </Section> + + <Section title={intl.formatMessage(messages.blocks)} onOpen={this.handleDomainBlocksOpen}> + {domainBlocks.get('isLoading') ? ( + <> + <Skeleton width='100%' /> + <br /> + <Skeleton width='70%' /> + </> + ) : (domainBlocks.get('isAvailable') ? ( + <> + <p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p> + + <table className='about__domain-blocks'> + <thead> + <tr> + <th><FormattedMessage id='about.domain_blocks.domain' defaultMessage='Domain' /></th> + <th><FormattedMessage id='about.domain_blocks.severity' defaultMessage='Severity' /></th> + <th><FormattedMessage id='about.domain_blocks.comment' defaultMessage='Reason' /></th> + </tr> + </thead> + + <tbody> + {domainBlocks.get('items').map(block => ( + <tr key={block.get('domain')}> + <td><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></td> + <td><span title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span></td> + <td>{block.get('comment')}</td> + </tr> + ))} + </tbody> + </table> + </> + ) : ( + <p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p> + ))} + </Section> + + <LinkFooter /> + </div> + + <Helmet> + <title>{intl.formatMessage(messages.title)}</title> + <meta name='robots' content='all' /> + </Helmet> + </Column> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/account/components/featured_tags.js b/app/javascript/flavours/glitch/features/account/components/featured_tags.js new file mode 100644 index 000000000..d646b08b2 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account/components/featured_tags.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Hashtag from 'flavours/glitch/components/hashtag'; + +const messages = defineMessages({ + lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' }, + empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' }, +}); + +export default @injectIntl +class FeaturedTags extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + account: ImmutablePropTypes.map, + featuredTags: ImmutablePropTypes.list, + tagged: PropTypes.string, + intl: PropTypes.object.isRequired, + }; + + render () { + const { account, featuredTags, intl } = this.props; + + if (!account || account.get('suspended') || featuredTags.isEmpty()) { + return null; + } + + return ( + <div className='getting-started__trends'> + <h4><FormattedMessage id='account.featured_tags.title' defaultMessage="{name}'s featured hashtags" values={{ name: <bdi dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /> }} /></h4> + + {featuredTags.take(3).map(featuredTag => ( + <Hashtag + key={featuredTag.get('name')} + name={featuredTag.get('name')} + href={featuredTag.get('url')} + to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`} + uses={featuredTag.get('statuses_count')} + withGraph={false} + description={((featuredTag.get('statuses_count') * 1) > 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)} + /> + ))} + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 0f0e40268..cf1494f05 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { autoPlayGif, me, title, domain } from 'flavours/glitch/initial_state'; +import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state'; import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links'; import classNames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; @@ -19,7 +19,7 @@ import { Helmet } from 'react-helmet'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, - cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' }, + cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, @@ -273,7 +273,9 @@ class Header extends ImmutablePureComponent { const content = { __html: account.get('note_emojified') }; const displayNameHtml = { __html: account.get('display_name_html') }; const fields = account.get('fields'); - const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); + const isLocal = account.get('acct').indexOf('@') === -1; + const acct = isLocal && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); + const isIndexable = !account.get('noindex'); let badge; @@ -352,7 +354,8 @@ class Header extends ImmutablePureComponent { </div> <Helmet> - <title>{titleFromAccount(account)} - {title}</title> + <title>{titleFromAccount(account)}</title> + <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} /> </Helmet> </div> ); diff --git a/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js b/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js new file mode 100644 index 000000000..6f0b06941 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import FeaturedTags from '../components/featured_tags'; +import { makeGetAccount } from 'flavours/glitch/selectors'; +import { List as ImmutableList } from 'immutable'; + +const mapStateToProps = () => { + const getAccount = makeGetAccount(); + + return (state, { accountId }) => ({ + account: getAccount(state, accountId), + featuredTags: state.getIn(['user_lists', 'featured_tags', accountId, 'items'], ImmutableList()), + }); +}; + +export default connect(mapStateToProps)(FeaturedTags); diff --git a/app/javascript/flavours/glitch/features/account/navigation.js b/app/javascript/flavours/glitch/features/account/navigation.js new file mode 100644 index 000000000..edae38ce5 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account/navigation.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import FeaturedTags from 'flavours/glitch/features/account/containers/featured_tags_container'; +import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; + +const mapStateToProps = (state, { match: { params: { acct } } }) => { + const accountId = state.getIn(['accounts_map', normalizeForLookup(acct)]); + + if (!accountId) { + return { + isLoading: true, + }; + } + + return { + accountId, + isLoading: false, + }; +}; + +export default @connect(mapStateToProps) +class AccountNavigation extends React.PureComponent { + + static propTypes = { + match: PropTypes.shape({ + params: PropTypes.shape({ + acct: PropTypes.string, + tagged: PropTypes.string, + }).isRequired, + }).isRequired, + + accountId: PropTypes.string, + isLoading: PropTypes.bool, + }; + + render () { + const { accountId, isLoading, match: { params: { tagged } } } = this.props; + + if (isLoading) { + return null; + } + + return ( + <> + <div className='flex-spacer' /> + <FeaturedTags accountId={accountId} tagged={tagged} /> + </> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js index 8df1bf4ca..638224bc0 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.js +++ b/app/javascript/flavours/glitch/features/account_gallery/index.js @@ -15,9 +15,10 @@ import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import LoadMore from 'flavours/glitch/components/load_more'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import { openModal } from 'flavours/glitch/actions/modal'; +import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; const mapStateToProps = (state, { params: { acct, id } }) => { - const accountId = id || state.getIn(['accounts_map', acct]); + const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); if (!accountId) { return { diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js index c1577e170..e6d8bb3bc 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js +++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js @@ -24,6 +24,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { unfollowModal } from 'flavours/glitch/initial_state'; const messages = defineMessages({ + cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' }, unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, }); @@ -43,7 +44,7 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch, { intl }) => ({ onFollow (account) { - if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { + if (account.getIn(['relationship', 'following'])) { if (unfollowModal) { dispatch(openModal('CONFIRM', { message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, @@ -53,6 +54,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } else { dispatch(unfollowAccount(account.get('id'))); } + } else if (account.getIn(['relationship', 'requested'])) { + if (unfollowModal) { + dispatch(openModal('CONFIRM', { + message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, + confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + })); + } } else { dispatch(followAccount(account.get('id'))); } diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index 68d558e66..b735af0ac 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -17,19 +17,22 @@ import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import TimelineHint from 'flavours/glitch/components/timeline_hint'; import LimitedAccountHint from './components/limited_account_hint'; import { getAccountHidden } from 'flavours/glitch/selectors'; +import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; +import { fetchFeaturedTags } from '../../actions/featured_tags'; const emptyList = ImmutableList(); -const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) => { - const accountId = id || state.getIn(['accounts_map', acct]); +const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = false }) => { + const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); if (!accountId) { return { isLoading: true, + statusIds: emptyList, }; } - const path = withReplies ? `${accountId}:with_replies` : accountId; + const path = withReplies ? `${accountId}:with_replies` : `${accountId}${tagged ? `:${tagged}` : ''}`; return { accountId, @@ -37,7 +40,7 @@ const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) = remoteUrl: state.getIn(['accounts', accountId, 'url']), isAccount: !!state.getIn(['accounts', accountId]), statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()), - featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()), + featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, 'items'], ImmutableList()), isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']), hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']), suspended: state.getIn(['accounts', accountId, 'suspended'], false), @@ -60,6 +63,7 @@ class AccountTimeline extends ImmutablePureComponent { params: PropTypes.shape({ acct: PropTypes.string, id: PropTypes.string, + tagged: PropTypes.string, }).isRequired, accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, @@ -77,14 +81,16 @@ class AccountTimeline extends ImmutablePureComponent { }; _load () { - const { accountId, withReplies, dispatch } = this.props; + const { accountId, withReplies, params: { tagged }, dispatch } = this.props; dispatch(fetchAccount(accountId)); if (!withReplies) { - dispatch(expandAccountFeaturedTimeline(accountId)); + dispatch(expandAccountFeaturedTimeline(accountId, { tagged })); } - dispatch(expandAccountTimeline(accountId, { withReplies })); + + dispatch(fetchFeaturedTags(accountId)); + dispatch(expandAccountTimeline(accountId, { withReplies, tagged })); } componentDidMount () { @@ -98,12 +104,17 @@ class AccountTimeline extends ImmutablePureComponent { } componentDidUpdate (prevProps) { - const { params: { acct }, accountId, dispatch } = this.props; + const { params: { acct, tagged }, accountId, withReplies, dispatch } = this.props; if (prevProps.accountId !== accountId && accountId) { this._load(); } else if (prevProps.params.acct !== acct) { dispatch(lookupAccount(acct)); + } else if (prevProps.params.tagged !== tagged) { + if (!withReplies) { + dispatch(expandAccountFeaturedTimeline(accountId, { tagged })); + } + dispatch(expandAccountTimeline(accountId, { withReplies, tagged })); } } @@ -126,7 +137,7 @@ class AccountTimeline extends ImmutablePureComponent { } handleLoadMore = maxId => { - this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies })); + this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies, tagged: this.props.params.tagged })); } setRef = c => { @@ -136,19 +147,17 @@ class AccountTimeline extends ImmutablePureComponent { render () { const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props; - if (!isAccount) { + if (isLoading && statusIds.isEmpty()) { return ( <Column> - <ColumnBackButton multiColumn={multiColumn} /> - <MissingIndicator /> + <LoadingIndicator /> </Column> ); - } - - if (!statusIds && isLoading) { + } else if (!isLoading && !isAccount) { return ( <Column> - <LoadingIndicator /> + <ColumnBackButton multiColumn={multiColumn} /> + <MissingIndicator /> </Column> ); } @@ -174,7 +183,7 @@ class AccountTimeline extends ImmutablePureComponent { <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} /> <StatusList - prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} />} + prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />} alwaysPrepend append={remoteMessage} scrollKey='account_timeline' diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js index ada8fb068..8978ac5fc 100644 --- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js +++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js @@ -1,15 +1,16 @@ -import React from 'react'; -import { connect } from 'react-redux'; +import { debounce } from 'lodash'; import PropTypes from 'prop-types'; +import React from 'react'; +import { Helmet } from 'react-helmet'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { debounce } from 'lodash'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'flavours/glitch/actions/bookmarks'; -import Column from 'flavours/glitch/features/ui/components/column'; -import ColumnHeader from 'flavours/glitch/components/column_header'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; +import ColumnHeader from 'flavours/glitch/components/column_header'; import StatusList from 'flavours/glitch/components/status_list'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import Column from 'flavours/glitch/features/ui/components/column'; const messages = defineMessages({ heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, @@ -95,6 +96,11 @@ class Bookmarks extends ImmutablePureComponent { emptyMessage={emptyMessage} bindToDocument={!multiColumn} /> + + <Helmet> + <title>{intl.formatMessage(messages.heading)}</title> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/closed_registrations_modal/index.js b/app/javascript/flavours/glitch/features/closed_registrations_modal/index.js new file mode 100644 index 000000000..cb91636cb --- /dev/null +++ b/app/javascript/flavours/glitch/features/closed_registrations_modal/index.js @@ -0,0 +1,75 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { domain } from 'flavours/glitch/initial_state'; +import { fetchServer } from 'flavours/glitch/actions/server'; + +const mapStateToProps = state => ({ + message: state.getIn(['server', 'server', 'registrations', 'message']), +}); + +export default @connect(mapStateToProps) +class ClosedRegistrationsModal extends ImmutablePureComponent { + + componentDidMount () { + const { dispatch } = this.props; + dispatch(fetchServer()); + } + + render () { + let closedRegistrationsMessage; + + if (this.props.message) { + closedRegistrationsMessage = ( + <p + className='prose' + dangerouslySetInnerHTML={{ __html: this.props.message }} + /> + ); + } else { + closedRegistrationsMessage = ( + <p className='prose'> + <FormattedMessage + id='closed_registrations_modal.description' + defaultMessage='Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.' + values={{ domain: <strong>{domain}</strong> }} + /> + </p> + ); + } + + return ( + <div className='modal-root__modal interaction-modal'> + <div className='interaction-modal__lead'> + <h3><FormattedMessage id='closed_registrations_modal.title' defaultMessage='Signing up on Mastodon' /></h3> + <p> + <FormattedMessage + id='closed_registrations_modal.preamble' + defaultMessage='Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!' + /> + </p> + </div> + + <div className='interaction-modal__choices'> + <div className='interaction-modal__choices__choice'> + <h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3> + {closedRegistrationsMessage} + </div> + + <div className='interaction-modal__choices__choice'> + <h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3> + <p className='prose'> + <FormattedMessage + id='closed_registrations.other_server_instructions' + defaultMessage='Since Mastodon is decentralized, you can create an account on another server and still interact with this one.' + /> + </p> + <a href='https://joinmastodon.org/servers' className='button button--block'><FormattedMessage id='closed_registrations_modal.find_another_server' defaultMessage='Find another server' /></a> + </div> + </div> + </div> + ); + } + +}; diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.js index 5ee46ca3b..67bf54875 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.js +++ b/app/javascript/flavours/glitch/features/community_timeline/index.js @@ -10,7 +10,8 @@ import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/col import ColumnSettingsContainer from './containers/column_settings_container'; import { connectCommunityStream } from 'flavours/glitch/actions/streaming'; import { Helmet } from 'react-helmet'; -import { title } from 'flavours/glitch/initial_state'; +import { domain } from 'flavours/glitch/initial_state'; +import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; const messages = defineMessages({ title: { id: 'column.community', defaultMessage: 'Local timeline' }, @@ -138,6 +139,10 @@ class CommunityTimeline extends React.PureComponent { <ColumnSettingsContainer columnId={columnId} /> </ColumnHeader> + <DismissableBanner id='community_timeline'> + <FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} /> + </DismissableBanner> + <StatusListContainer trackScroll={!pinned} scrollKey={`community_timeline-${columnId}`} @@ -149,7 +154,8 @@ class CommunityTimeline extends React.PureComponent { /> <Helmet> - <title>{intl.formatMessage(messages.title)} - {title}</title> + <title>{intl.formatMessage(messages.title)}</title> + <meta name='robots' content='noindex' /> </Helmet> </Column> ); diff --git a/app/javascript/flavours/glitch/features/compose/components/search.js b/app/javascript/flavours/glitch/features/compose/components/search.js index a59418e46..326fe5b70 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search.js +++ b/app/javascript/flavours/glitch/features/compose/components/search.js @@ -21,6 +21,7 @@ import Motion from '../../ui/util/optional_motion'; const messages = defineMessages({ placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, + placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' }, }); class SearchPopout extends React.PureComponent { @@ -62,6 +63,7 @@ class Search extends React.PureComponent { static contextTypes = { router: PropTypes.object.isRequired, + identity: PropTypes.object.isRequired, }; static propTypes = { @@ -137,6 +139,7 @@ class Search extends React.PureComponent { render () { const { intl, value, submitted } = this.props; const { expanded } = this.state; + const { signedIn } = this.context.identity; const hasValue = value.length > 0 || submitted; return ( @@ -147,7 +150,7 @@ class Search extends React.PureComponent { ref={this.setRef} className='search__input' type='text' - placeholder={intl.formatMessage(messages.placeholder)} + placeholder={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)} value={value || ''} onChange={this.handleChange} onKeyUp={this.handleKeyUp} diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_form.js b/app/javascript/flavours/glitch/features/compose/components/upload_form.js index 43039c674..35880ddcc 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/upload_form.js @@ -4,7 +4,6 @@ import UploadProgressContainer from '../containers/upload_progress_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import UploadContainer from '../containers/upload_container'; import SensitiveButtonContainer from '../containers/sensitive_button_container'; -import { FormattedMessage } from 'react-intl'; export default class UploadForm extends ImmutablePureComponent { static propTypes = { @@ -16,7 +15,7 @@ export default class UploadForm extends ImmutablePureComponent { return ( <div className='composer--upload_form'> - <UploadProgressContainer icon='upload' message={<FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />} /> + <UploadProgressContainer /> {mediaIds.size > 0 && ( <div className='content'> diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_progress.js b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js index 8896bbffd..c7c33c418 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload_progress.js +++ b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js @@ -3,26 +3,34 @@ import PropTypes from 'prop-types'; import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import Icon from 'flavours/glitch/components/icon'; +import { FormattedMessage } from 'react-intl'; export default class UploadProgress extends React.PureComponent { static propTypes = { active: PropTypes.bool, progress: PropTypes.number, - icon: PropTypes.string.isRequired, - message: PropTypes.node.isRequired, + isProcessing: PropTypes.bool, }; render () { - const { active, progress, icon, message } = this.props; + const { active, progress, isProcessing } = this.props; if (!active) { return null; } + let message; + + if (isProcessing) { + message = <FormattedMessage id='upload_progress.processing' defaultMessage='Processing…' />; + } else { + message = <FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />; + } + return ( <div className='composer--upload_form--progress'> - <Icon id={icon} /> + <Icon id='upload' /> <div className='message'> {message} diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js index 0cfee96da..b18c76a43 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js @@ -4,6 +4,7 @@ import UploadProgress from '../components/upload_progress'; const mapStateToProps = state => ({ active: state.getIn(['compose', 'is_uploading']), progress: state.getIn(['compose', 'progress']), + isProcessing: state.getIn(['compose', 'is_processing']), }); export default connect(mapStateToProps)(UploadProgress); diff --git a/app/javascript/flavours/glitch/features/compose/index.js b/app/javascript/flavours/glitch/features/compose/index.js index 567bb3711..8ca378672 100644 --- a/app/javascript/flavours/glitch/features/compose/index.js +++ b/app/javascript/flavours/glitch/features/compose/index.js @@ -14,6 +14,8 @@ import SearchResultsContainer from './containers/search_results_container'; import { me, mascot } from 'flavours/glitch/initial_state'; import { cycleElefriendCompose } from 'flavours/glitch/actions/compose'; import HeaderContainer from './containers/header_container'; +import Column from 'flavours/glitch/components/column'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' }, @@ -21,7 +23,7 @@ const messages = defineMessages({ const mapStateToProps = (state, ownProps) => ({ elefriend: state.getIn(['compose', 'elefriend']), - showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : ownProps.isSearchPage, + showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : false, }); const mapDispatchToProps = (dispatch, { intl }) => ({ @@ -44,7 +46,6 @@ class Compose extends React.PureComponent { static propTypes = { multiColumn: PropTypes.bool, showSearch: PropTypes.bool, - isSearchPage: PropTypes.bool, elefriend: PropTypes.number, onClickElefriend: PropTypes.func, onMount: PropTypes.func, @@ -53,19 +54,11 @@ class Compose extends React.PureComponent { }; componentDidMount () { - const { isSearchPage } = this.props; - - if (!isSearchPage) { - this.props.onMount(); - } + this.props.onMount(); } componentWillUnmount () { - const { isSearchPage } = this.props; - - if (!isSearchPage) { - this.props.onUnmount(); - } + this.props.onUnmount(); } render () { @@ -74,37 +67,49 @@ class Compose extends React.PureComponent { intl, multiColumn, onClickElefriend, - isSearchPage, showSearch, } = this.props; const computedClass = classNames('drawer', `mbstobon-${elefriend}`); - return ( - <div className={computedClass} role='region' aria-label={intl.formatMessage(messages.compose)}> - {multiColumn && <HeaderContainer />} + if (multiColumn) { + return ( + <div className={computedClass} role='region' aria-label={intl.formatMessage(messages.compose)}> + <HeaderContainer /> - {(multiColumn || isSearchPage) && <SearchContainer />} + {multiColumn && <SearchContainer />} - <div className='drawer__pager'> - {!isSearchPage && <div className='drawer__inner'> - <NavigationContainer /> + <div className='drawer__pager'> + <div className='drawer__inner'> + <NavigationContainer /> - <ComposeFormContainer /> + <ComposeFormContainer /> - <div className='drawer__inner__mastodon'> - {mascot ? <img alt='' draggable='false' src={mascot} /> : <button className='mastodon' onClick={onClickElefriend} />} + <div className='drawer__inner__mastodon'> + {mascot ? <img alt='' draggable='false' src={mascot} /> : <button className='mastodon' onClick={onClickElefriend} />} + </div> </div> - </div>} - <Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}> - {({ x }) => ( - <div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}> - <SearchResultsContainer /> - </div> - )} - </Motion> + <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}> + {({ x }) => ( + <div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}> + <SearchResultsContainer /> + </div> + )} + </Motion> + </div> </div> - </div> + ); + } + + return ( + <Column> + <NavigationContainer /> + <ComposeFormContainer /> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> + </Column> ); } diff --git a/app/javascript/flavours/glitch/features/direct_timeline/index.js b/app/javascript/flavours/glitch/features/direct_timeline/index.js index 75ca19d17..d55c63c2b 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/index.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/index.js @@ -1,15 +1,16 @@ +import PropTypes from 'prop-types'; import React from 'react'; +import { Helmet } from 'react-helmet'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; +import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; +import { mountConversations, unmountConversations, expandConversations } from 'flavours/glitch/actions/conversations'; +import { connectDirectStream } from 'flavours/glitch/actions/streaming'; +import { expandDirectTimeline } from 'flavours/glitch/actions/timelines'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; -import { expandDirectTimeline } from 'flavours/glitch/actions/timelines'; -import { mountConversations, unmountConversations, expandConversations } from 'flavours/glitch/actions/conversations'; -import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; import ColumnSettingsContainer from './containers/column_settings_container'; -import { connectDirectStream } from 'flavours/glitch/actions/streaming'; import ConversationsListContainer from './containers/conversations_list_container'; const messages = defineMessages({ @@ -143,6 +144,11 @@ class DirectTimeline extends React.PureComponent { </ColumnHeader> {contents} + + <Helmet> + <title>{intl.formatMessage(messages.title)}</title> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/directory/components/account_card.js b/app/javascript/flavours/glitch/features/directory/components/account_card.js index b6785a5f9..8c344c793 100644 --- a/app/javascript/flavours/glitch/features/directory/components/account_card.js +++ b/app/javascript/flavours/glitch/features/directory/components/account_card.js @@ -24,7 +24,7 @@ import classNames from 'classnames'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, - cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' }, + cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' }, unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' }, diff --git a/app/javascript/flavours/glitch/features/directory/index.js b/app/javascript/flavours/glitch/features/directory/index.js index dce8fa4e7..94bcd578c 100644 --- a/app/javascript/flavours/glitch/features/directory/index.js +++ b/app/javascript/flavours/glitch/features/directory/index.js @@ -13,7 +13,6 @@ import RadioButton from 'flavours/glitch/components/radio_button'; import LoadMore from 'flavours/glitch/components/load_more'; import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; -import { title } from 'flavours/glitch/initial_state'; import { Helmet } from 'react-helmet'; const messages = defineMessages({ @@ -169,7 +168,8 @@ class Directory extends React.PureComponent { {multiColumn && !pinned ? <ScrollContainer scrollKey='directory'>{scrollableArea}</ScrollContainer> : scrollableArea} <Helmet> - <title>{intl.formatMessage(messages.title)} - {title}</title> + <title>{intl.formatMessage(messages.title)}</title> + <meta name='robots' content='noindex' /> </Helmet> </Column> ); diff --git a/app/javascript/flavours/glitch/features/domain_blocks/index.js b/app/javascript/flavours/glitch/features/domain_blocks/index.js index acce87d5a..cb0b55c63 100644 --- a/app/javascript/flavours/glitch/features/domain_blocks/index.js +++ b/app/javascript/flavours/glitch/features/domain_blocks/index.js @@ -11,6 +11,7 @@ import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_bloc import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' }, @@ -59,6 +60,7 @@ class Blocks extends ImmutablePureComponent { return ( <Column bindToDocument={!multiColumn} icon='minus-circle' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> + <ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore} @@ -70,6 +72,10 @@ class Blocks extends ImmutablePureComponent { <DomainContainer key={domain} domain={domain} />, )} </ScrollableList> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/explore/index.js b/app/javascript/flavours/glitch/features/explore/index.js index f37793732..24fa26eec 100644 --- a/app/javascript/flavours/glitch/features/explore/index.js +++ b/app/javascript/flavours/glitch/features/explore/index.js @@ -13,7 +13,6 @@ import Search from 'flavours/glitch/features/compose/containers/search_container import SearchResults from './results'; import { showTrends } from 'flavours/glitch/initial_state'; import { Helmet } from 'react-helmet'; -import { title } from 'flavours/glitch/initial_state'; const messages = defineMessages({ title: { id: 'explore.title', defaultMessage: 'Explore' }, @@ -85,7 +84,8 @@ class Explore extends React.PureComponent { </Switch> <Helmet> - <title>{intl.formatMessage(messages.title)} - {title}</title> + <title>{intl.formatMessage(messages.title)}</title> + <meta name='robots' content={isSearching ? 'noindex' : 'all'} /> </Helmet> </React.Fragment> )} diff --git a/app/javascript/flavours/glitch/features/explore/links.js b/app/javascript/flavours/glitch/features/explore/links.js index 6adc2f6fb..092f86b29 100644 --- a/app/javascript/flavours/glitch/features/explore/links.js +++ b/app/javascript/flavours/glitch/features/explore/links.js @@ -6,6 +6,7 @@ import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import { connect } from 'react-redux'; import { fetchTrendingLinks } from 'flavours/glitch/actions/trends'; import { FormattedMessage } from 'react-intl'; +import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; const mapStateToProps = state => ({ links: state.getIn(['trends', 'links', 'items']), @@ -29,9 +30,17 @@ class Links extends React.PureComponent { render () { const { isLoading, links } = this.props; + const banner = ( + <DismissableBanner id='explore/links'> + <FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These news stories are being talked about by people on this and other servers of the decentralized network right now.' /> + </DismissableBanner> + ); + if (!isLoading && links.isEmpty()) { return ( <div className='explore__links scrollable scrollable--flex'> + {banner} + <div className='empty-column-indicator'> <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' /> </div> @@ -41,6 +50,8 @@ class Links extends React.PureComponent { return ( <div className='explore__links'> + {banner} + {isLoading ? (<LoadingIndicator />) : links.map(link => ( <Story key={link.get('id')} diff --git a/app/javascript/flavours/glitch/features/explore/results.js b/app/javascript/flavours/glitch/features/explore/results.js index e37379686..892980d95 100644 --- a/app/javascript/flavours/glitch/features/explore/results.js +++ b/app/javascript/flavours/glitch/features/explore/results.js @@ -10,7 +10,6 @@ import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag' import { List as ImmutableList } from 'immutable'; import LoadMore from 'flavours/glitch/components/load_more'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; -import { title } from 'flavours/glitch/initial_state'; import { Helmet } from 'react-helmet'; const messages = defineMessages({ @@ -118,7 +117,7 @@ class Results extends React.PureComponent { </div> <Helmet> - <title>{intl.formatMessage(messages.title, { q })} - {title}</title> + <title>{intl.formatMessage(messages.title, { q })}</title> </Helmet> </React.Fragment> ); diff --git a/app/javascript/flavours/glitch/features/explore/statuses.js b/app/javascript/flavours/glitch/features/explore/statuses.js index fe08ce466..0a5c9de23 100644 --- a/app/javascript/flavours/glitch/features/explore/statuses.js +++ b/app/javascript/flavours/glitch/features/explore/statuses.js @@ -6,6 +6,7 @@ import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { fetchTrendingStatuses, expandTrendingStatuses } from 'flavours/glitch/actions/trends'; import { debounce } from 'lodash'; +import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; const mapStateToProps = state => ({ statusIds: state.getIn(['status_lists', 'trending', 'items']), @@ -40,17 +41,23 @@ class Statuses extends React.PureComponent { const emptyMessage = <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />; return ( - <StatusList - trackScroll - statusIds={statusIds} - scrollKey='explore-statuses' - hasMore={hasMore} - isLoading={isLoading} - onLoadMore={this.handleLoadMore} - emptyMessage={emptyMessage} - bindToDocument={!multiColumn} - withCounters - /> + <> + <DismissableBanner id='explore/statuses'> + <FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These posts from this and other servers in the decentralized network are gaining traction on this server right now.' /> + </DismissableBanner> + + <StatusList + trackScroll + statusIds={statusIds} + scrollKey='explore-statuses' + hasMore={hasMore} + isLoading={isLoading} + onLoadMore={this.handleLoadMore} + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} + withCounters + /> + </> ); } diff --git a/app/javascript/flavours/glitch/features/explore/tags.js b/app/javascript/flavours/glitch/features/explore/tags.js index 465fad0df..938036b64 100644 --- a/app/javascript/flavours/glitch/features/explore/tags.js +++ b/app/javascript/flavours/glitch/features/explore/tags.js @@ -6,6 +6,7 @@ import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import { connect } from 'react-redux'; import { fetchTrendingHashtags } from 'flavours/glitch/actions/trends'; import { FormattedMessage } from 'react-intl'; +import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; const mapStateToProps = state => ({ hashtags: state.getIn(['trends', 'tags', 'items']), @@ -29,9 +30,17 @@ class Tags extends React.PureComponent { render () { const { isLoading, hashtags } = this.props; + const banner = ( + <DismissableBanner id='explore/tags'> + <FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These hashtags are gaining traction among people on this and other servers of the decentralized network right now.' /> + </DismissableBanner> + ); + if (!isLoading && hashtags.isEmpty()) { return ( <div className='explore__links scrollable scrollable--flex'> + {banner} + <div className='empty-column-indicator'> <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' /> </div> @@ -41,6 +50,8 @@ class Tags extends React.PureComponent { return ( <div className='explore__links'> + {banner} + {isLoading ? (<LoadingIndicator />) : hashtags.map(hashtag => ( <Hashtag key={hashtag.get('name')} hashtag={hashtag} /> ))} diff --git a/app/javascript/flavours/glitch/features/favourited_statuses/index.js b/app/javascript/flavours/glitch/features/favourited_statuses/index.js index 4df3aaa64..a03e1a4eb 100644 --- a/app/javascript/flavours/glitch/features/favourited_statuses/index.js +++ b/app/javascript/flavours/glitch/features/favourited_statuses/index.js @@ -1,15 +1,16 @@ -import React from 'react'; -import { connect } from 'react-redux'; +import { debounce } from 'lodash'; import PropTypes from 'prop-types'; +import React from 'react'; +import { Helmet } from 'react-helmet'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { debounce } from 'lodash'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { connect } from 'react-redux'; +import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'flavours/glitch/actions/favourites'; -import Column from 'flavours/glitch/features/ui/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; -import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import StatusList from 'flavours/glitch/components/status_list'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import Column from 'flavours/glitch/features/ui/components/column'; const messages = defineMessages({ heading: { id: 'column.favourites', defaultMessage: 'Favourites' }, @@ -95,6 +96,11 @@ class Favourites extends ImmutablePureComponent { emptyMessage={emptyMessage} bindToDocument={!multiColumn} /> + + <Helmet> + <title>{intl.formatMessage(messages.heading)}</title> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/favourites/index.js b/app/javascript/flavours/glitch/features/favourites/index.js index a51562e71..47c3279c4 100644 --- a/app/javascript/flavours/glitch/features/favourites/index.js +++ b/app/javascript/flavours/glitch/features/favourites/index.js @@ -1,16 +1,17 @@ -import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { connect } from 'react-redux'; +import ColumnHeader from 'flavours/glitch/components/column_header'; +import Icon from 'flavours/glitch/components/icon'; import { fetchFavourites } from 'flavours/glitch/actions/interactions'; +import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; import AccountContainer from 'flavours/glitch/containers/account_container'; import Column from 'flavours/glitch/features/ui/components/column'; -import Icon from 'flavours/glitch/components/icon'; -import ColumnHeader from 'flavours/glitch/components/column_header'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ScrollableList from '../../components/scrollable_list'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ heading: { id: 'column.favourited_by', defaultMessage: 'Favourited by' }, @@ -91,6 +92,10 @@ class Favourites extends ImmutablePureComponent { <AccountContainer key={id} id={id} withNote={false} />, )} </ScrollableList> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/follow_recommendations/index.js b/app/javascript/flavours/glitch/features/follow_recommendations/index.js index f934aeb35..d9d962b7c 100644 --- a/app/javascript/flavours/glitch/features/follow_recommendations/index.js +++ b/app/javascript/flavours/glitch/features/follow_recommendations/index.js @@ -12,6 +12,7 @@ import Column from 'flavours/glitch/features/ui/components/column'; import Account from './components/account'; import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg'; import Button from 'flavours/glitch/components/button'; +import { Helmet } from 'react-helmet'; const mapStateToProps = state => ({ suggestions: state.getIn(['suggestions', 'items']), @@ -104,6 +105,10 @@ class FollowRecommendations extends ImmutablePureComponent { </React.Fragment> )} </div> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/follow_requests/index.js b/app/javascript/flavours/glitch/features/follow_requests/index.js index 47ca1e1bf..7b35e3ec9 100644 --- a/app/javascript/flavours/glitch/features/follow_requests/index.js +++ b/app/javascript/flavours/glitch/features/follow_requests/index.js @@ -12,6 +12,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; import { me } from 'flavours/glitch/initial_state'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' }, @@ -88,6 +89,10 @@ class FollowRequests extends ImmutablePureComponent { <AccountAuthorizeContainer key={id} id={id} />, )} </ScrollableList> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js index 27a63b3fd..7122c1905 100644 --- a/app/javascript/flavours/glitch/features/followers/index.js +++ b/app/javascript/flavours/glitch/features/followers/index.js @@ -21,9 +21,10 @@ import ScrollableList from 'flavours/glitch/components/scrollable_list'; import TimelineHint from 'flavours/glitch/components/timeline_hint'; import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; import { getAccountHidden } from 'flavours/glitch/selectors'; +import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; const mapStateToProps = (state, { params: { acct, id } }) => { - const accountId = id || state.getIn(['accounts_map', acct]); + const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); if (!accountId) { return { diff --git a/app/javascript/flavours/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js index aa187bf95..4ad670105 100644 --- a/app/javascript/flavours/glitch/features/following/index.js +++ b/app/javascript/flavours/glitch/features/following/index.js @@ -21,9 +21,10 @@ import ScrollableList from 'flavours/glitch/components/scrollable_list'; import TimelineHint from 'flavours/glitch/components/timeline_hint'; import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; import { getAccountHidden } from 'flavours/glitch/selectors'; +import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; const mapStateToProps = (state, { params: { acct, id } }) => { - const accountId = id || state.getIn(['accounts_map', acct]); + const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); if (!accountId) { return { diff --git a/app/javascript/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js index f52b769a3..f9d79013b 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.js +++ b/app/javascript/flavours/glitch/features/getting_started/index.js @@ -17,6 +17,7 @@ import { preferencesLink } from 'flavours/glitch/utils/backend_links'; import NavigationBar from '../compose/components/navigation_bar'; import LinkFooter from 'flavours/glitch/features/ui/components/link_footer'; import TrendsContainer from './containers/trends_container'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, @@ -84,11 +85,12 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2); static contextTypes = { router: PropTypes.object.isRequired, + identity: PropTypes.object, }; static propTypes = { intl: PropTypes.object.isRequired, - myAccount: ImmutablePropTypes.map.isRequired, + myAccount: ImmutablePropTypes.map, columns: ImmutablePropTypes.list, multiColumn: PropTypes.bool, fetchFollowRequests: PropTypes.func.isRequired, @@ -104,10 +106,10 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2); } componentDidMount () { - const { fetchFollowRequests, multiColumn } = this.props; + const { fetchFollowRequests } = this.props; + const { signedIn } = this.context.identity; - if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) { - this.context.router.history.replace('/home'); + if (!signedIn) { return; } @@ -116,12 +118,13 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2); render () { const { intl, myAccount, columns, multiColumn, unreadFollowRequests, unreadNotifications, lists, openSettings } = this.props; + const { signedIn } = this.context.identity; const navItems = []; let listItems = []; if (multiColumn) { - if (!columns.find(item => item.get('id') === 'HOME')) { + if (signedIn && !columns.find(item => item.get('id') === 'HOME')) { navItems.push(<ColumnLink key='home' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/home' />); } @@ -142,47 +145,58 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2); navItems.push(<ColumnLink key='explore' icon='hashtag' text={intl.formatMessage(messages.explore)} to='/explore' />); } - if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) { - navItems.push(<ColumnLink key='conversations' icon='envelope' text={intl.formatMessage(messages.direct)} to='/conversations' />); - } + if (signedIn) { + if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) { + navItems.push(<ColumnLink key='conversations' icon='envelope' text={intl.formatMessage(messages.direct)} to='/conversations' />); + } - if (!multiColumn || !columns.find(item => item.get('id') === 'BOOKMARKS')) { - navItems.push(<ColumnLink key='bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />); - } + if (!multiColumn || !columns.find(item => item.get('id') === 'BOOKMARKS')) { + navItems.push(<ColumnLink key='bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />); + } - if (myAccount.get('locked') || unreadFollowRequests > 0) { - navItems.push(<ColumnLink key='follow_requests' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />); - } + if (myAccount.get('locked') || unreadFollowRequests > 0) { + navItems.push(<ColumnLink key='follow_requests' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />); + } - navItems.push(<ColumnLink key='getting_started' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />); + navItems.push(<ColumnLink key='getting_started' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />); - listItems = listItems.concat([ - <div key='9'> - <ColumnLink key='lists' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' /> - {lists.filter(list => !columns.find(item => item.get('id') === 'LIST' && item.getIn(['params', 'id']) === list.get('id'))).map(list => - <ColumnLink key={`list-${list.get('id')}`} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} /> - )} - </div>, - ]); + listItems = listItems.concat([ + <div key='9'> + <ColumnLink key='lists' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' /> + {lists.filter(list => !columns.find(item => item.get('id') === 'LIST' && item.getIn(['params', 'id']) === list.get('id'))).map(list => + <ColumnLink key={`list-${list.get('id')}`} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} /> + )} + </div>, + ]); + } return ( <Column bindToDocument={!multiColumn} name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile> <div className='scrollable optionally-scrollable'> <div className='getting-started__wrapper'> - {!multiColumn && <NavigationBar account={myAccount} />} + {!multiColumn && signedIn && <NavigationBar account={myAccount} />} {multiColumn && <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />} {navItems} - <ColumnSubheading text={intl.formatMessage(messages.lists_subheading)} /> - {listItems} - <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} /> - { preferencesLink !== undefined && <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href={preferencesLink} /> } - <ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={openSettings} /> + {signedIn && ( + <React.Fragment> + <ColumnSubheading text={intl.formatMessage(messages.lists_subheading)} /> + {listItems} + <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} /> + { preferencesLink !== undefined && <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href={preferencesLink} /> } + <ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={openSettings} /> + </React.Fragment> + )} </div> <LinkFooter /> </div> {multiColumn && showTrends && <TrendsContainer />} + + <Helmet> + <title>{intl.formatMessage(messages.menu)}</title> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js index 5e098514a..f1827789f 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js @@ -14,7 +14,6 @@ import { isEqual } from 'lodash'; import { fetchHashtag, followHashtag, unfollowHashtag } from 'flavours/glitch/actions/tags'; import Icon from 'flavours/glitch/components/icon'; import classNames from 'classnames'; -import { title } from 'flavours/glitch/initial_state'; import { Helmet } from 'react-helmet'; const messages = defineMessages({ @@ -228,7 +227,8 @@ class HashtagTimeline extends React.PureComponent { /> <Helmet> - <title>{`#${id}`} - {title}</title> + <title>#{id}</title> + <meta name='robots' content='noindex' /> </Helmet> </Column> ); diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js index 86aaa0258..23d0440a9 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.js +++ b/app/javascript/flavours/glitch/features/home_timeline/index.js @@ -15,7 +15,6 @@ import classNames from 'classnames'; import IconWithBadge from 'flavours/glitch/components/icon_with_badge'; import NotSignedInIndicator from 'flavours/glitch/components/not_signed_in_indicator'; import { Helmet } from 'react-helmet'; -import { title } from 'flavours/glitch/initial_state'; const messages = defineMessages({ title: { id: 'column.home', defaultMessage: 'Home' }, @@ -170,7 +169,8 @@ class HomeTimeline extends React.PureComponent { ) : <NotSignedInIndicator />} <Helmet> - <title>{intl.formatMessage(messages.title)} - {title}</title> + <title>{intl.formatMessage(messages.title)}</title> + <meta name='robots' content='noindex' /> </Helmet> </Column> ); diff --git a/app/javascript/flavours/glitch/features/interaction_modal/index.js b/app/javascript/flavours/glitch/features/interaction_modal/index.js index 5623f8a06..4cd8e51de 100644 --- a/app/javascript/flavours/glitch/features/interaction_modal/index.js +++ b/app/javascript/flavours/glitch/features/interaction_modal/index.js @@ -5,11 +5,19 @@ import { registrationsOpen } from 'flavours/glitch/initial_state'; import { connect } from 'react-redux'; import Icon from 'flavours/glitch/components/icon'; import classNames from 'classnames'; +import { openModal, closeModal } from 'flavours/glitch/actions/modal'; const mapStateToProps = (state, { accountId }) => ({ displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']), }); +const mapDispatchToProps = (dispatch) => ({ + onSignupClick() { + dispatch(closeModal()); + dispatch(openModal('CLOSED_REGISTRATIONS')); + }, +}); + class Copypaste extends React.PureComponent { static propTypes = { @@ -66,15 +74,20 @@ class Copypaste extends React.PureComponent { } -export default @connect(mapStateToProps) +export default @connect(mapStateToProps, mapDispatchToProps) class InteractionModal extends React.PureComponent { static propTypes = { displayNameHtml: PropTypes.string, url: PropTypes.string, type: PropTypes.oneOf(['reply', 'reblog', 'favourite', 'follow']), + onSignupClick: PropTypes.func.isRequired, }; + handleSignupClick = () => { + this.props.onSignupClick(); + } + render () { const { url, type, displayNameHtml } = this.props; @@ -105,6 +118,22 @@ class InteractionModal extends React.PureComponent { break; } + let signupButton; + + if (registrationsOpen) { + signupButton = ( + <a href='/auth/sign_up' className='button button--block button-tertiary'> + <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /> + </a> + ); + } else { + signupButton = ( + <button className='button button--block button-tertiary' onClick={this.handleSignupClick}> + <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /> + </button> + ); + } + return ( <div className='modal-root__modal interaction-modal'> <div className='interaction-modal__lead'> @@ -116,7 +145,7 @@ class InteractionModal extends React.PureComponent { <div className='interaction-modal__choices__choice'> <h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3> <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a> - <a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a> + {signupButton} </div> <div className='interaction-modal__choices__choice'> diff --git a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js index 481f76763..2bc0116d4 100644 --- a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js +++ b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js @@ -1,10 +1,11 @@ import React from 'react'; -import Column from 'flavours/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; +import Column from 'flavours/glitch/components/column'; import { connect } from 'react-redux'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import ColumnHeader from 'flavours/glitch/components/column_header'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' }, @@ -28,8 +29,13 @@ class KeyboardShortcuts extends ImmutablePureComponent { const { intl, collapseEnabled, multiColumn } = this.props; return ( - <Column bindToDocument={!multiColumn} icon='question' heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> + <Column> + <ColumnHeader + title={intl.formatMessage(messages.heading)} + icon='question' + multiColumn={multiColumn} + /> + <div className='keyboard-shortcuts scrollable optionally-scrollable'> <table> <thead> @@ -132,6 +138,10 @@ class KeyboardShortcuts extends ImmutablePureComponent { </tbody> </table> </div> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.js b/app/javascript/flavours/glitch/features/list_timeline/index.js index 08347446b..a94c05c56 100644 --- a/app/javascript/flavours/glitch/features/list_timeline/index.js +++ b/app/javascript/flavours/glitch/features/list_timeline/index.js @@ -1,20 +1,22 @@ -import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import React from 'react'; +import { Helmet } from 'react-helmet'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; -import Column from 'flavours/glitch/components/column'; -import ColumnHeader from 'flavours/glitch/components/column_header'; -import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; -import { connectListStream } from 'flavours/glitch/actions/streaming'; -import { expandListTimeline } from 'flavours/glitch/actions/timelines'; +import { connect } from 'react-redux'; +import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import { fetchList, deleteList, updateList } from 'flavours/glitch/actions/lists'; import { openModal } from 'flavours/glitch/actions/modal'; -import MissingIndicator from 'flavours/glitch/components/missing_indicator'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; +import { connectListStream } from 'flavours/glitch/actions/streaming'; +import { expandListTimeline } from 'flavours/glitch/actions/timelines'; +import Column from 'flavours/glitch/components/column'; +import ColumnBackButton from 'flavours/glitch/components/column_back_button'; +import ColumnHeader from 'flavours/glitch/components/column_header'; import Icon from 'flavours/glitch/components/icon'; +import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; +import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import RadioButton from 'flavours/glitch/components/radio_button'; +import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; const messages = defineMessages({ deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' }, @@ -210,6 +212,11 @@ class ListTimeline extends React.PureComponent { emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet.' />} bindToDocument={!multiColumn} /> + + <Helmet> + <title>{title}</title> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/lists/index.js b/app/javascript/flavours/glitch/features/lists/index.js index b92389d82..8773be5e6 100644 --- a/app/javascript/flavours/glitch/features/lists/index.js +++ b/app/javascript/flavours/glitch/features/lists/index.js @@ -1,18 +1,19 @@ -import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import React from 'react'; +import { Helmet } from 'react-helmet'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchLists } from 'flavours/glitch/actions/lists'; +import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; import Column from 'flavours/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; -import { fetchLists } from 'flavours/glitch/actions/lists'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; import ColumnLink from 'flavours/glitch/features/ui/components/column_link'; import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading'; import NewListForm from './components/new_list_form'; -import { createSelector } from 'reselect'; -import ScrollableList from 'flavours/glitch/components/scrollable_list'; const messages = defineMessages({ heading: { id: 'column.lists', defaultMessage: 'Lists' }, @@ -76,6 +77,11 @@ class Lists extends ImmutablePureComponent { <ColumnLink key={list.get('id')} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} />, )} </ScrollableList> + + <Helmet> + <title>{intl.formatMessage(messages.heading)}</title> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } 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 e154fe1c8..d01eec811 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js @@ -181,36 +181,6 @@ class LocalSettingsPage extends React.PureComponent { <FormattedMessage id='settings.wide_view' defaultMessage='Wide view (Desktop mode only)' /> <span className='hint'><FormattedMessage id='settings.wide_view_hint' defaultMessage='Stretches columns to better fill the available space.' /></span> </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['navbar_under']} - id='mastodon-settings--navbar_under' - onChange={onChange} - > - <FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' /> - </LocalSettingsPageItem> - <DeprecatedLocalSettingsPageItem - id='mastodon-settings--swipe_to_change_columns' - value={!disableSwiping} - > - <FormattedMessage id='settings.swipe_to_change_columns' defaultMessage='Allow swiping to change columns (Mobile only)' /> - <span className='hint'> - <FormattedMessage - id='settings.deprecated_setting' - defaultMessage="This setting is now controlled from Mastodon's {settings_page_link}" - values={{ - settings_page_link: ( - <a href={preferenceLink('user_setting_disable_swiping')}> - <FormattedMessage - id='settings.shared_settings_link' - defaultMessage='user preferences' - /> - </a> - ) - }} - /> - </span> - </DeprecatedLocalSettingsPageItem> </section> </div> ), diff --git a/app/javascript/flavours/glitch/features/mutes/index.js b/app/javascript/flavours/glitch/features/mutes/index.js index 764cbef1a..8da106e47 100644 --- a/app/javascript/flavours/glitch/features/mutes/index.js +++ b/app/javascript/flavours/glitch/features/mutes/index.js @@ -11,6 +11,7 @@ import AccountContainer from 'flavours/glitch/containers/account_container'; import { fetchMutes, expandMutes } from 'flavours/glitch/actions/mutes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ heading: { id: 'column.mutes', defaultMessage: 'Muted users' }, @@ -72,6 +73,10 @@ class Mutes extends ImmutablePureComponent { <AccountContainer key={id} id={id} defaultAction='mute' />, )} </ScrollableList> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/notifications/components/admin_report.js b/app/javascript/flavours/glitch/features/notifications/components/admin_report.js index 80beeb9da..4662bd953 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/admin_report.js +++ b/app/javascript/flavours/glitch/features/notifications/components/admin_report.js @@ -69,6 +69,10 @@ export default class AdminReport extends ImmutablePureComponent { render () { const { intl, account, notification, unread, report } = this.props; + if (!report) { + return null; + } + // Links to the display name. const displayName = account.get('display_name_html') || account.get('username'); const link = ( diff --git a/app/javascript/flavours/glitch/features/notifications/components/column_settings.js b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js index 42ab9de35..ee05c7fd6 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/column_settings.js +++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js @@ -171,7 +171,7 @@ export default class ColumnSettings extends React.PureComponent { </div> </div> - {(this.context.identity.permissions & PERMISSION_MANAGE_USERS === PERMISSION_MANAGE_USERS) && ( + {((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && ( <div role='group' aria-labelledby='notifications-admin-sign-up'> <span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></span> @@ -184,7 +184,7 @@ export default class ColumnSettings extends React.PureComponent { </div> )} - {(this.context.identity.permissions & PERMISSION_MANAGE_REPORTS === PERMISSION_MANAGE_REPORTS) && ( + {((this.context.identity.permissions & PERMISSION_MANAGE_REPORTS) === PERMISSION_MANAGE_REPORTS) && ( <div role='group' aria-labelledby='notifications-admin-report'> <span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></span> diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index 26eeba168..67b155ced 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -30,7 +30,6 @@ import compareId from 'flavours/glitch/compare_id'; import NotificationsPermissionBanner from './components/notifications_permission_banner'; import NotSignedInIndicator from 'flavours/glitch/components/not_signed_in_indicator'; import { Helmet } from 'react-helmet'; -import { title } from 'flavours/glitch/initial_state'; import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container'; @@ -373,7 +372,8 @@ class Notifications extends React.PureComponent { {scrollContainer} <Helmet> - <title>{intl.formatMessage(messages.title)} - {title}</title> + <title>{intl.formatMessage(messages.title)}</title> + <meta name='robots' content='noindex' /> </Helmet> </Column> ); diff --git a/app/javascript/flavours/glitch/features/pinned_statuses/index.js b/app/javascript/flavours/glitch/features/pinned_statuses/index.js index 518d0294b..eeeab46ab 100644 --- a/app/javascript/flavours/glitch/features/pinned_statuses/index.js +++ b/app/javascript/flavours/glitch/features/pinned_statuses/index.js @@ -8,6 +8,7 @@ import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_ import StatusList from 'flavours/glitch/components/status_list'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ heading: { id: 'column.pins', defaultMessage: 'Pinned post' }, @@ -54,6 +55,9 @@ class PinnedStatuses extends ImmutablePureComponent { hasMore={hasMore} bindToDocument={!multiColumn} /> + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/privacy_policy/index.js b/app/javascript/flavours/glitch/features/privacy_policy/index.js new file mode 100644 index 000000000..4618d9e32 --- /dev/null +++ b/app/javascript/flavours/glitch/features/privacy_policy/index.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Helmet } from 'react-helmet'; +import { FormattedMessage, FormattedDate, injectIntl, defineMessages } from 'react-intl'; +import Column from 'flavours/glitch/components/column'; +import api from 'flavours/glitch/api'; +import Skeleton from 'flavours/glitch/components/skeleton'; + +const messages = defineMessages({ + title: { id: 'privacy_policy.title', defaultMessage: 'Privacy Policy' }, +}); + +export default @injectIntl +class PrivacyPolicy extends React.PureComponent { + + static propTypes = { + intl: PropTypes.object, + multiColumn: PropTypes.bool, + }; + + state = { + content: null, + lastUpdated: null, + isLoading: true, + }; + + componentDidMount () { + api().get('/api/v1/instance/privacy_policy').then(({ data }) => { + this.setState({ content: data.content, lastUpdated: data.updated_at, isLoading: false }); + }).catch(() => { + this.setState({ isLoading: false }); + }); + } + + render () { + const { intl, multiColumn } = this.props; + const { isLoading, content, lastUpdated } = this.state; + + return ( + <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}> + <div className='scrollable privacy-policy'> + <div className='column-title'> + <h3><FormattedMessage id='privacy_policy.title' defaultMessage='Privacy Policy' /></h3> + <p><FormattedMessage id='privacy_policy.last_updated' defaultMessage='Last updated {date}' values={{ date: isLoading ? <Skeleton width='10ch' /> : <FormattedDate value={lastUpdated} year='numeric' month='short' day='2-digit' /> }} /></p> + </div> + + <div + className='privacy-policy__body prose' + dangerouslySetInnerHTML={{ __html: content }} + /> + </div> + + <Helmet> + <title>{intl.formatMessage(messages.title)}</title> + <meta name='robots' content='all' /> + </Helmet> + </Column> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.js b/app/javascript/flavours/glitch/features/public_timeline/index.js index aa70acf15..a61a47de1 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/index.js +++ b/app/javascript/flavours/glitch/features/public_timeline/index.js @@ -10,7 +10,7 @@ import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/col import ColumnSettingsContainer from './containers/column_settings_container'; import { connectPublicStream } from 'flavours/glitch/actions/streaming'; import { Helmet } from 'react-helmet'; -import { title } from 'flavours/glitch/initial_state'; +import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; const messages = defineMessages({ title: { id: 'column.public', defaultMessage: 'Federated timeline' }, @@ -143,6 +143,10 @@ class PublicTimeline extends React.PureComponent { <ColumnSettingsContainer columnId={columnId} /> </ColumnHeader> + <DismissableBanner id='public_timeline'> + <FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.' /> + </DismissableBanner> + <StatusListContainer timelineId={`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`} onLoadMore={this.handleLoadMore} @@ -154,7 +158,8 @@ class PublicTimeline extends React.PureComponent { /> <Helmet> - <title>{intl.formatMessage(messages.title)} - {title}</title> + <title>{intl.formatMessage(messages.title)}</title> + <meta name='robots' content='noindex' /> </Helmet> </Column> ); diff --git a/app/javascript/flavours/glitch/features/reblogs/index.js b/app/javascript/flavours/glitch/features/reblogs/index.js index ed646c6ed..b097ff9d7 100644 --- a/app/javascript/flavours/glitch/features/reblogs/index.js +++ b/app/javascript/flavours/glitch/features/reblogs/index.js @@ -11,6 +11,7 @@ import ColumnHeader from 'flavours/glitch/components/column_header'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ heading: { id: 'column.reblogged_by', defaultMessage: 'Boosted by' }, @@ -92,6 +93,10 @@ class Reblogs extends ImmutablePureComponent { <AccountContainer key={id} id={id} withNote={false} />, )} </ScrollableList> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/report/category.js b/app/javascript/flavours/glitch/features/report/category.js index 43fb7a17c..55c43577b 100644 --- a/app/javascript/flavours/glitch/features/report/category.js +++ b/app/javascript/flavours/glitch/features/report/category.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import Button from 'flavours/glitch/components/button'; import Option from './components/option'; +import { List as ImmutableList } from 'immutable'; const messages = defineMessages({ dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' }, @@ -20,7 +21,7 @@ const messages = defineMessages({ }); const mapStateToProps = state => ({ - rules: state.getIn(['server', 'rules']), + rules: state.getIn(['server', 'server', 'rules'], ImmutableList()), }); export default @connect(mapStateToProps) diff --git a/app/javascript/flavours/glitch/features/report/rules.js b/app/javascript/flavours/glitch/features/report/rules.js index 599c04dbd..efcdf1fcf 100644 --- a/app/javascript/flavours/glitch/features/report/rules.js +++ b/app/javascript/flavours/glitch/features/report/rules.js @@ -7,7 +7,7 @@ import Button from 'flavours/glitch/components/button'; import Option from './components/option'; const mapStateToProps = state => ({ - rules: state.getIn(['server', 'rules']), + rules: state.getIn(['server', 'server', 'rules']), }); export default @connect(mapStateToProps) diff --git a/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js deleted file mode 100644 index 629f5c2ea..000000000 --- a/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { expandHashtagTimeline } from 'flavours/glitch/actions/timelines'; -import Masonry from 'react-masonry-infinite'; -import { List as ImmutableList } from 'immutable'; -import DetailedStatusContainer from 'flavours/glitch/features/status/containers/detailed_status_container'; -import { debounce } from 'lodash'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; - -const mapStateToProps = (state, { hashtag }) => ({ - statusIds: state.getIn(['timelines', `hashtag:${hashtag}`, 'items'], ImmutableList()), - isLoading: state.getIn(['timelines', `hashtag:${hashtag}`, 'isLoading'], false), - hasMore: state.getIn(['timelines', `hashtag:${hashtag}`, 'hasMore'], false), -}); - -export default @connect(mapStateToProps) -class HashtagTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - statusIds: ImmutablePropTypes.list.isRequired, - isLoading: PropTypes.bool.isRequired, - hasMore: PropTypes.bool.isRequired, - hashtag: PropTypes.string.isRequired, - local: PropTypes.bool.isRequired, - }; - - static defaultProps = { - local: false, - }; - - componentDidMount () { - const { dispatch, hashtag, local } = this.props; - - dispatch(expandHashtagTimeline(hashtag, { local })); - } - - handleLoadMore = () => { - const { dispatch, hashtag, local, statusIds } = this.props; - const maxId = statusIds.last(); - - if (maxId) { - dispatch(expandHashtagTimeline(hashtag, { maxId, local })); - } - } - - setRef = c => { - this.masonry = c; - } - - handleHeightChange = debounce(() => { - if (!this.masonry) { - return; - } - - this.masonry.forcePack(); - }, 50) - - render () { - const { statusIds, hasMore, isLoading } = this.props; - - const sizes = [ - { columns: 1, gutter: 0 }, - { mq: '415px', columns: 1, gutter: 10 }, - { mq: '640px', columns: 2, gutter: 10 }, - { mq: '960px', columns: 3, gutter: 10 }, - { mq: '1255px', columns: 3, gutter: 10 }, - ]; - - const loader = (isLoading && statusIds.isEmpty()) ? <LoadingIndicator key={0} /> : undefined; - - return ( - <Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}> - {statusIds.map(statusId => ( - <div className='statuses-grid__item' key={statusId}> - <DetailedStatusContainer - id={statusId} - compact - measureHeight - onHeightChange={this.handleHeightChange} - /> - </div> - )).toArray()} - </Masonry> - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/standalone/public_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/public_timeline/index.js deleted file mode 100644 index 5f8a369ff..000000000 --- a/app/javascript/flavours/glitch/features/standalone/public_timeline/index.js +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; -import Masonry from 'react-masonry-infinite'; -import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; -import DetailedStatusContainer from 'flavours/glitch/features/status/containers/detailed_status_container'; -import { debounce } from 'lodash'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; - -const mapStateToProps = (state, { local }) => { - const timeline = state.getIn(['timelines', local ? 'community' : 'public'], ImmutableMap()); - - return { - statusIds: timeline.get('items', ImmutableList()), - isLoading: timeline.get('isLoading', false), - hasMore: timeline.get('hasMore', false), - }; -}; - -export default @connect(mapStateToProps) -class PublicTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - statusIds: ImmutablePropTypes.list.isRequired, - isLoading: PropTypes.bool.isRequired, - hasMore: PropTypes.bool.isRequired, - local: PropTypes.bool, - }; - - componentDidMount () { - this._connect(); - } - - componentDidUpdate (prevProps) { - if (prevProps.local !== this.props.local) { - this._disconnect(); - this._connect(); - } - } - - _connect () { - const { dispatch, local } = this.props; - - dispatch(local ? expandCommunityTimeline() : expandPublicTimeline()); - } - - handleLoadMore = () => { - const { dispatch, statusIds, local } = this.props; - const maxId = statusIds.last(); - - if (maxId) { - dispatch(local ? expandCommunityTimeline({ maxId }) : expandPublicTimeline({ maxId })); - } - } - - setRef = c => { - this.masonry = c; - } - - handleHeightChange = debounce(() => { - if (!this.masonry) { - return; - } - - this.masonry.forcePack(); - }, 50) - - render () { - const { statusIds, hasMore, isLoading } = this.props; - - const sizes = [ - { columns: 1, gutter: 0 }, - { mq: '415px', columns: 1, gutter: 10 }, - { mq: '640px', columns: 2, gutter: 10 }, - { mq: '960px', columns: 3, gutter: 10 }, - { mq: '1255px', columns: 3, gutter: 10 }, - ]; - - const loader = (isLoading && statusIds.isEmpty()) ? <LoadingIndicator key={0} /> : undefined; - - return ( - <Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}> - {statusIds.map(statusId => ( - <div className='statuses-grid__item' key={statusId}> - <DetailedStatusContainer - id={statusId} - compact - measureHeight - onHeightChange={this.handleHeightChange} - /> - </div> - )).toArray()} - </Masonry> - ); - } - -} 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 9868621fe..0e21ca5cc 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -175,7 +175,7 @@ class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); menu.push(null); - // menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); + menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); } else { diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index c967ef34d..f59e8c7f6 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { createSelector } from 'reselect'; import { fetchStatus } from 'flavours/glitch/actions/statuses'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; +import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import DetailedStatus from './components/detailed_status'; import ActionBar from './components/action_bar'; import Column from 'flavours/glitch/features/ui/components/column'; @@ -47,7 +48,7 @@ import { openModal } from 'flavours/glitch/actions/modal'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { HotKeys } from 'react-hotkeys'; -import { boostModal, favouriteModal, deleteModal, title } from 'flavours/glitch/initial_state'; +import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/initial_state'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning'; import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/components/status'; @@ -135,6 +136,7 @@ const makeMapStateToProps = () => { } return { + isLoading: state.getIn(['statuses', props.params.statusId, 'isLoading']), status, ancestorsIds, descendantsIds, @@ -178,6 +180,7 @@ class Status extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, status: ImmutablePropTypes.map, + isLoading: PropTypes.bool, settings: ImmutablePropTypes.map.isRequired, ancestorsIds: ImmutablePropTypes.list, descendantsIds: ImmutablePropTypes.list, @@ -589,9 +592,17 @@ class Status extends ImmutablePureComponent { render () { let ancestors, descendants; - const { status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, usingPiP } = this.props; + const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, usingPiP } = this.props; const { fullscreen } = this.state; + if (isLoading) { + return ( + <Column> + <LoadingIndicator /> + </Column> + ); + } + if (status === null) { return ( <Column> @@ -611,6 +622,9 @@ class Status extends ImmutablePureComponent { descendants = <div>{this.renderChildren(descendantsIds)}</div>; } + const isLocal = status.getIn(['account', 'acct'], '').indexOf('@') === -1; + const isIndexable = !status.getIn(['account', 'noindex']); + const handlers = { moveUp: this.handleHotkeyMoveUp, moveDown: this.handleHotkeyMoveDown, @@ -684,7 +698,8 @@ class Status extends ImmutablePureComponent { </ScrollContainer> <Helmet> - <title>{titleFromStatus(status)} - {title}</title> + <title>{titleFromStatus(status)}</title> + <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} /> </Helmet> </Column> ); diff --git a/app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js b/app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js index 3e979a250..7cbe1413d 100644 --- a/app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js +++ b/app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js @@ -1,44 +1,162 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import Column from 'flavours/glitch/components/column'; +import Button from 'flavours/glitch/components/button'; +import { Helmet } from 'react-helmet'; +import { Link } from 'react-router-dom'; +import classNames from 'classnames'; +import { autoPlayGif } from 'flavours/glitch/initial_state'; -import Column from './column'; -import ColumnHeader from './column_header'; -import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; -import IconButton from 'flavours/glitch/components/icon_button'; +class GIF extends React.PureComponent { -const messages = defineMessages({ - title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' }, - body: { id: 'bundle_column_error.body', defaultMessage: 'Something went wrong while loading this component.' }, - retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' }, -}); + static propTypes = { + src: PropTypes.string.isRequired, + staticSrc: PropTypes.string.isRequired, + className: PropTypes.string, + animate: PropTypes.bool, + }; + + static defaultProps = { + animate: autoPlayGif, + }; + + state = { + hovering: false, + }; + + handleMouseEnter = () => { + const { animate } = this.props; + + if (!animate) { + this.setState({ hovering: true }); + } + } -class BundleColumnError extends React.Component { + handleMouseLeave = () => { + const { animate } = this.props; + + if (!animate) { + this.setState({ hovering: false }); + } + } + + render () { + const { src, staticSrc, className, animate } = this.props; + const { hovering } = this.state; + + return ( + <img + className={className} + src={(hovering || animate) ? src : staticSrc} + alt='' + role='presentation' + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} + /> + ); + } + +} + +class CopyButton extends React.PureComponent { static propTypes = { - onRetry: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, + children: PropTypes.node.isRequired, + value: PropTypes.string.isRequired, + }; + + state = { + copied: false, + }; + + handleClick = () => { + const { value } = this.props; + navigator.clipboard.writeText(value); + this.setState({ copied: true }); + this.timeout = setTimeout(() => this.setState({ copied: false }), 700); + } + + componentWillUnmount () { + if (this.timeout) clearTimeout(this.timeout); } + render () { + const { children } = this.props; + const { copied } = this.state; + + return ( + <Button onClick={this.handleClick} className={copied ? 'copied' : 'copyable'}>{copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : children}</Button> + ); + } + +} + +export default @injectIntl +class BundleColumnError extends React.PureComponent { + + static propTypes = { + errorType: PropTypes.oneOf(['routing', 'network', 'error']), + onRetry: PropTypes.func, + intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, + stacktrace: PropTypes.string, + }; + + static defaultProps = { + errorType: 'routing', + }; + handleRetry = () => { - this.props.onRetry(); + const { onRetry } = this.props; + + if (onRetry) { + onRetry(); + } } render () { - const { intl: { formatMessage } } = this.props; + const { errorType, multiColumn, stacktrace } = this.props; + + let title, body; + + switch(errorType) { + case 'routing': + title = <FormattedMessage id='bundle_column_error.routing.title' defaultMessage='404' />; + body = <FormattedMessage id='bundle_column_error.routing.body' defaultMessage='The requested page could not be found. Are you sure the URL in the address bar is correct?' />; + break; + case 'network': + title = <FormattedMessage id='bundle_column_error.network.title' defaultMessage='Network error' />; + body = <FormattedMessage id='bundle_column_error.network.body' defaultMessage='There was an error when trying to load this page. This could be due to a temporary problem with your internet connection or this server.' />; + break; + case 'error': + title = <FormattedMessage id='bundle_column_error.error.title' defaultMessage='Oh, no!' />; + body = <FormattedMessage id='bundle_column_error.error.body' defaultMessage='The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.' />; + break; + } return ( - <Column> - <ColumnHeader icon='exclamation-circle' type={formatMessage(messages.title)} /> - <ColumnBackButtonSlim /> + <Column bindToDocument={!multiColumn}> <div className='error-column'> - <IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} /> - {formatMessage(messages.body)} + <GIF src='/oops.gif' staticSrc='/oops.png' className='error-column__image' /> + + <div className='error-column__message'> + <h1>{title}</h1> + <p>{body}</p> + + <div className='error-column__message__actions'> + {errorType === 'network' && <Button onClick={this.handleRetry}><FormattedMessage id='bundle_column_error.retry' defaultMessage='Try again' /></Button>} + {errorType === 'error' && <CopyButton value={stacktrace}><FormattedMessage id='bundle_column_error.copy_stacktrace' defaultMessage='Copy error report' /></CopyButton>} + <Link to='/' className={classNames('button', { 'button-tertiary': errorType !== 'routing' })}><FormattedMessage id='bundle_column_error.return' defaultMessage='Go back home' /></Link> + </div> + </div> </div> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> </Column> ); } } - -export default injectIntl(BundleColumnError); diff --git a/app/javascript/flavours/glitch/features/ui/components/column_link.js b/app/javascript/flavours/glitch/features/ui/components/column_link.js index d04b869b6..4f04fdba2 100644 --- a/app/javascript/flavours/glitch/features/ui/components/column_link.js +++ b/app/javascript/flavours/glitch/features/ui/components/column_link.js @@ -1,26 +1,29 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; +import { NavLink } from 'react-router-dom'; import Icon from 'flavours/glitch/components/icon'; +import classNames from 'classnames'; -const ColumnLink = ({ icon, text, to, onClick, href, method, badge }) => { +const ColumnLink = ({ icon, text, to, onClick, href, method, badge, transparent, ...other }) => { + const className = classNames('column-link', { 'column-link--transparent': transparent }); const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null; + const iconElement = typeof icon === 'string' ? <Icon id={icon} fixedWidth className='column-link__icon' /> : icon; if (href) { return ( - <a href={href} className='column-link' data-method={method}> - <Icon id={icon} fixedWidth className='column-link__icon' /> - {text} + <a href={href} className={className} data-method={method} title={text} {...other}> + {iconElement} + <span>{text}</span> {badgeElement} </a> ); } else if (to) { return ( - <Link to={to} className='column-link'> - <Icon id={icon} fixedWidth className='column-link__icon' /> - {text} + <NavLink to={to} className={className} title={text} {...other}> + {iconElement} + <span>{text}</span> {badgeElement} - </Link> + </NavLink> ); } else { const handleOnClick = (e) => { @@ -29,8 +32,8 @@ const ColumnLink = ({ icon, text, to, onClick, href, method, badge }) => { return onClick(e); } return ( - <a href='#' onClick={onClick && handleOnClick} className='column-link' tabIndex='0'> - <Icon id={icon} fixedWidth className='column-link__icon' /> + <a href='#' onClick={onClick && handleOnClick} className={className} title={text} {...other} tabIndex='0'> + {iconElement} {text} {badgeElement} </a> @@ -39,13 +42,14 @@ const ColumnLink = ({ icon, text, to, onClick, href, method, badge }) => { }; ColumnLink.propTypes = { - icon: PropTypes.string.isRequired, + icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, text: PropTypes.string.isRequired, to: PropTypes.string, onClick: PropTypes.func, href: PropTypes.string, method: PropTypes.string, badge: PropTypes.node, + transparent: PropTypes.bool, }; export default ColumnLink; diff --git a/app/javascript/flavours/glitch/features/ui/components/column_loading.js b/app/javascript/flavours/glitch/features/ui/components/column_loading.js index 22c00c915..b07385397 100644 --- a/app/javascript/flavours/glitch/features/ui/components/column_loading.js +++ b/app/javascript/flavours/glitch/features/ui/components/column_loading.js @@ -10,6 +10,7 @@ export default class ColumnLoading extends ImmutablePureComponent { static propTypes = { title: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), icon: PropTypes.string, + multiColumn: PropTypes.bool, }; static defaultProps = { @@ -18,10 +19,11 @@ export default class ColumnLoading extends ImmutablePureComponent { }; render() { - let { title, icon } = this.props; + let { title, icon, multiColumn } = this.props; + return ( <Column> - <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} placeholder /> + <ColumnHeader icon={icon} title={title} multiColumn={multiColumn} focusable={false} placeholder /> <div className='scrollable' /> </Column> ); 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 718b4a27f..bf3e79c24 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js @@ -1,15 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -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 TabsBar, { links, getIndex, getLink } from './tabs_bar'; -import { Link } from 'react-router-dom'; - -import { disableSwiping } from 'flavours/glitch/initial_state'; - import BundleContainer from '../containers/bundle_container'; import ColumnLoading from './column_loading'; import DrawerLoading from './drawer_loading'; @@ -27,7 +19,6 @@ import { ListTimeline, Directory, } from '../../ui/util/async-components'; -import Icon from 'flavours/glitch/components/icon'; import ComposePanel from './compose_panel'; import NavigationPanel from './navigation_panel'; @@ -49,22 +40,13 @@ const componentMap = { 'DIRECTORY': Directory, }; -const shouldHideFAB = path => path.match(/^\/statuses\/|^\/@[^/]+\/\d+|^\/publish|^\/explore|^\/getting-started|^\/start/); - -const messages = defineMessages({ - publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }, -}); - -export default @(component => injectIntl(component, { withRef: true })) -class ColumnsArea extends ImmutablePureComponent { +export default class ColumnsArea extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object.isRequired, - identity: PropTypes.object.isRequired, }; static propTypes = { - intl: PropTypes.object.isRequired, columns: ImmutablePropTypes.list.isRequired, singleColumn: PropTypes.bool, children: PropTypes.node, @@ -72,20 +54,13 @@ class ColumnsArea extends ImmutablePureComponent { openSettings: PropTypes.func, }; - // Corresponds to (max-width: 600px + (285px * 1) + (10px * 1)) in SCSS - mediaQuery = 'matchMedia' in window && window.matchMedia('(max-width: 895px)'); + // Corresponds to (max-width: $no-gap-breakpoint + 285px - 1px) in SCSS + mediaQuery = 'matchMedia' in window && window.matchMedia('(max-width: 1174px)'); state = { - shouldAnimate: false, renderComposePanel: !(this.mediaQuery && this.mediaQuery.matches), } - componentWillReceiveProps() { - if (typeof this.pendingIndex !== 'number' && this.lastIndex !== getIndex(this.context.router.history.location.pathname)) { - this.setState({ shouldAnimate: false }); - } - } - componentDidMount() { if (!this.props.singleColumn) { this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); @@ -100,10 +75,7 @@ class ColumnsArea extends ImmutablePureComponent { this.setState({ renderComposePanel: !this.mediaQuery.matches }); } - this.lastIndex = getIndex(this.context.router.history.location.pathname); this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl'); - - this.setState({ shouldAnimate: true }); } componentWillUpdate(nextProps) { @@ -116,13 +88,6 @@ class ColumnsArea extends ImmutablePureComponent { if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) { this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); } - - const newIndex = getIndex(this.context.router.history.location.pathname); - - if (this.lastIndex !== newIndex) { - this.lastIndex = newIndex; - this.setState({ shouldAnimate: true }); - } } componentWillUnmount () { @@ -150,31 +115,6 @@ class ColumnsArea extends ImmutablePureComponent { this.setState({ renderComposePanel: !e.matches }); } - handleSwipe = (index) => { - this.pendingIndex = index; - - const nextLinkTranslationId = links[index].props['data-preview-title-id']; - const currentLinkSelector = '.tabs-bar__link.active'; - const nextLinkSelector = `.tabs-bar__link[data-preview-title-id="${nextLinkTranslationId}"]`; - - // HACK: Remove the active class from the current link and set it to the next one - // React-router does this for us, but too late, feeling laggy. - document.querySelector(currentLinkSelector).classList.remove('active'); - document.querySelector(nextLinkSelector).classList.add('active'); - - if (!this.state.shouldAnimate && typeof this.pendingIndex === 'number') { - this.context.router.history.push(getLink(this.pendingIndex)); - this.pendingIndex = null; - } - } - - handleAnimationEnd = () => { - if (typeof this.pendingIndex === 'number') { - this.context.router.history.push(getLink(this.pendingIndex)); - this.pendingIndex = null; - } - } - handleWheel = () => { if (typeof this._interruptScrollAnimation !== 'function') { return; @@ -187,48 +127,19 @@ class ColumnsArea extends ImmutablePureComponent { this.node = node; } - renderView = (link, index) => { - const columnIndex = getIndex(this.context.router.history.location.pathname); - const title = this.props.intl.formatMessage({ id: link.props['data-preview-title-id'] }); - const icon = link.props['data-preview-icon']; - - const view = (index === columnIndex) ? - React.cloneElement(this.props.children) : - <ColumnLoading title={title} icon={icon} />; - - return ( - <div className='columns-area columns-area--mobile' key={index}> - {view} - </div> - ); - } - renderLoading = columnId => () => { - return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading />; + return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />; } renderError = (props) => { - return <BundleColumnError {...props} />; + return <BundleColumnError multiColumn errorType='network' {...props} />; } render () { - const { columns, children, singleColumn, intl, navbarUnder, openSettings } = this.props; - const { shouldAnimate, renderComposePanel } = this.state; - const { signedIn } = this.context.identity; - - const columnIndex = getIndex(this.context.router.history.location.pathname); + const { columns, children, singleColumn, navbarUnder, openSettings } = this.props; + const { renderComposePanel } = this.state; if (singleColumn) { - const floatingActionButton = (!signedIn || shouldHideFAB(this.context.router.history.location.pathname)) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>; - - const content = columnIndex !== -1 ? ( - <ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={disableSwiping}> - {links.map(this.renderView)} - </ReactSwipeableViews> - ) : ( - <div key='content' className='columns-area columns-area--mobile'>{children}</div> - ); - return ( <div className='columns-area__panels'> <div className='columns-area__panels__pane columns-area__panels__pane--compositional'> @@ -237,10 +148,9 @@ class ColumnsArea extends ImmutablePureComponent { </div> </div> - <div className={`columns-area__panels__main ${floatingActionButton && 'with-fab'}`}> - {!navbarUnder && <TabsBar key='tabs' />} - {content} - {navbarUnder && <TabsBar key='tabs' />} + <div className='columns-area__panels__main'> + <div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div> + <div className='columns-area columns-area--mobile'>{children}</div> </div> <div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'> @@ -248,8 +158,6 @@ class ColumnsArea extends ImmutablePureComponent { <NavigationPanel onOpenSettings={openSettings} /> </div> </div> - - {floatingActionButton} </div> ); } diff --git a/app/javascript/flavours/glitch/features/ui/components/compose_panel.js b/app/javascript/flavours/glitch/features/ui/components/compose_panel.js index 6e1c51d74..dde252a61 100644 --- a/app/javascript/flavours/glitch/features/ui/components/compose_panel.js +++ b/app/javascript/flavours/glitch/features/ui/components/compose_panel.js @@ -1,18 +1,34 @@ import React from 'react'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import SearchContainer from 'flavours/glitch/features/compose/containers/search_container'; import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container'; import NavigationContainer from 'flavours/glitch/features/compose/containers/navigation_container'; import LinkFooter from './link_footer'; import ServerBanner from 'flavours/glitch/components/server_banner'; +import { mountCompose, unmountCompose } from 'flavours/glitch/actions/compose'; -export default +export default @connect() class ComposePanel extends React.PureComponent { static contextTypes = { identity: PropTypes.object.isRequired, }; + static propTypes = { + dispatch: PropTypes.func.isRequired, + }; + + componentDidMount () { + const { dispatch } = this.props; + dispatch(mountCompose()); + } + + componentWillUnmount () { + const { dispatch } = this.props; + dispatch(unmountCompose()); + } + render() { const { signedIn } = this.context.identity; @@ -34,7 +50,7 @@ class ComposePanel extends React.PureComponent { </React.Fragment> )} - <LinkFooter withHotkeys /> + <LinkFooter /> </div> ); } diff --git a/app/javascript/flavours/glitch/features/ui/components/follow_requests_nav_link.js b/app/javascript/flavours/glitch/features/ui/components/follow_requests_column_link.js index c30427896..301392a52 100644 --- a/app/javascript/flavours/glitch/features/ui/components/follow_requests_nav_link.js +++ b/app/javascript/flavours/glitch/features/ui/components/follow_requests_column_link.js @@ -2,22 +2,27 @@ import React from 'react'; import PropTypes from 'prop-types'; import { fetchFollowRequests } from 'flavours/glitch/actions/accounts'; import { connect } from 'react-redux'; -import { NavLink, withRouter } from 'react-router-dom'; +import ColumnLink from 'flavours/glitch/features/ui/components/column_link'; import IconWithBadge from 'flavours/glitch/components/icon_with_badge'; import { List as ImmutableList } from 'immutable'; -import { FormattedMessage } from 'react-intl'; +import { injectIntl, defineMessages } from 'react-intl'; + +const messages = defineMessages({ + text: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, +}); const mapStateToProps = state => ({ count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size, }); -export default @withRouter +export default @injectIntl @connect(mapStateToProps) -class FollowRequestsNavLink extends React.Component { +class FollowRequestsColumnLink extends React.Component { static propTypes = { dispatch: PropTypes.func.isRequired, count: PropTypes.number.isRequired, + intl: PropTypes.object.isRequired, }; componentDidMount () { @@ -27,13 +32,20 @@ class FollowRequestsNavLink extends React.Component { } render () { - const { count } = this.props; + const { count, intl } = this.props; if (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>; + return ( + <ColumnLink + transparent + to='/follow_requests' + icon={<IconWithBadge className='column-link__icon' id='user-plus' count={count} />} + text={intl.formatMessage(messages.text)} + /> + ); } } diff --git a/app/javascript/flavours/glitch/features/ui/components/header.js b/app/javascript/flavours/glitch/features/ui/components/header.js new file mode 100644 index 000000000..6c2fb40ba --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/components/header.js @@ -0,0 +1,63 @@ +import React from 'react'; +import Logo from 'flavours/glitch/components/logo'; +import { Link, withRouter } from 'react-router-dom'; +import { FormattedMessage } from 'react-intl'; +import { registrationsOpen, me } from 'flavours/glitch/initial_state'; +import Avatar from 'flavours/glitch/components/avatar'; +import Permalink from 'flavours/glitch/components/permalink'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +const Account = connect(state => ({ + account: state.getIn(['accounts', me]), +}))(({ account }) => ( + <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} title={account.get('acct')}> + <Avatar account={account} size={35} /> + </Permalink> +)); + +export default @withRouter +class Header extends React.PureComponent { + + static contextTypes = { + identity: PropTypes.object, + }; + + static propTypes = { + location: PropTypes.object, + }; + + render () { + const { signedIn } = this.context.identity; + const { location } = this.props; + + let content; + + if (signedIn) { + content = ( + <> + {location.pathname !== '/publish' && <Link to='/publish' className='button'><FormattedMessage id='compose_form.publish' defaultMessage='Publish' /></Link>} + <Account /> + </> + ); + } else { + content = ( + <> + <a href='/auth/sign_in' className='button'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a> + <a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a> + </> + ); + } + + return ( + <div className='ui__header'> + <Link to='/' className='ui__header__logo'><Logo /></Link> + + <div className='ui__header__links'> + {content} + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.js b/app/javascript/flavours/glitch/features/ui/components/link_footer.js index 39576f17b..2e061f760 100644 --- a/app/javascript/flavours/glitch/features/ui/components/link_footer.js +++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.js @@ -3,8 +3,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { Link } from 'react-router-dom'; -import { limitedFederationMode, version, repository, source_url, profile_directory as profileDirectory } from 'flavours/glitch/initial_state'; -import { signOutLink, securityLink, privacyPolicyLink } from 'flavours/glitch/utils/backend_links'; +import { version, repository, source_url, profile_directory as profileDirectory } from 'flavours/glitch/initial_state'; import { logOut } from 'flavours/glitch/utils/log_out'; import { openModal } from 'flavours/glitch/actions/modal'; import { PERMISSION_INVITE_USERS } from 'flavours/glitch/permissions'; @@ -34,7 +33,6 @@ class LinkFooter extends React.PureComponent { }; static propTypes = { - withHotkeys: PropTypes.bool, onLogout: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; @@ -49,44 +47,26 @@ class LinkFooter extends React.PureComponent { } render () { - const { withHotkeys } = this.props; const { signedIn, permissions } = this.context.identity; - const items = []; - if ((this.context.identity.permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) { - items.push(<a key='invites' href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a>); - } - - if (signedIn && withHotkeys) { - items.push(<Link key='hotkeys' to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link>); - } - - if (signedIn && securityLink) { - items.push(<a key='security' href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a>); - } - - if (!limitedFederationMode) { - items.push(<a key='about' href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a>); - } + items.push(<a key='apps' href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Get the app' /></a>); + items.push(<Link key='about' to='/about'><FormattedMessage id='navigation_bar.info' defaultMessage='About' /></Link>); + items.push(<a key='mastodon' href='https://joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.what_is_mastodon' defaultMessage='About Mastodon' /></a>); + items.push(<a key='docs' href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a>); + items.push(<Link key='privacy-policy' to='/privacy-policy'><FormattedMessage id='getting_started.privacy_policy' defaultMessage='Privacy Policy' /></Link>); + items.push(<Link key='hotkeys' to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link>); if (profileDirectory) { - items.push(<Link key='directory' to='/directory'><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></Link>); - } - - items.push(<a key='apps' href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a>); - - if (privacyPolicyLink) { - items.push(<a key='terms' href={privacyPolicyLink} target='_blank'><FormattedMessage id='getting_started.privacy_policy' defaultMessage='Privacy Policy' /></a>); + items.push(<Link key='directory' to='/directory'><FormattedMessage id='getting_started.directory' defaultMessage='Directory' /></Link>); } if (signedIn) { - items.push(<a key='developers' href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a>); - } + if ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) { + items.push(<a key='invites' href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a>); + } - items.push(<a key='docs' href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a>); - - if (signedIn) { + items.push(<a key='security' href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a>); items.push(<a key='logout' href='/auth/sign_out' onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a>); } diff --git a/app/javascript/flavours/glitch/features/ui/components/list_panel.js b/app/javascript/flavours/glitch/features/ui/components/list_panel.js index e61234283..dff830065 100644 --- a/app/javascript/flavours/glitch/features/ui/components/list_panel.js +++ b/app/javascript/flavours/glitch/features/ui/components/list_panel.js @@ -1,12 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { createSelector } from 'reselect'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { fetchLists } from 'flavours/glitch/actions/lists'; import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { NavLink, withRouter } from 'react-router-dom'; -import Icon from 'flavours/glitch/components/icon'; +import { withRouter } from 'react-router-dom'; +import { fetchLists } from 'flavours/glitch/actions/lists'; +import ColumnLink from './column_link'; const getOrderedLists = createSelector([state => state.get('lists')], lists => { if (!lists) { @@ -42,11 +42,11 @@ class ListPanel extends ImmutablePureComponent { } return ( - <div> + <div className='list-panel'> <hr /> {lists.map(list => ( - <NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/lists/${list.get('id')}`}><Icon className='column-link__icon' id='list-ul' fixedWidth />{list.get('title')}</NavLink> + <ColumnLink icon='list-ul' key={list.get('id')} strict text={list.get('title')} to={`/lists/${list.get('id')}`} transparent /> ))} </div> ); diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js index cedfabe03..93834f60e 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js @@ -13,10 +13,8 @@ import FavouriteModal from './favourite_modal'; import AudioModal from './audio_modal'; import DoodleModal from './doodle_modal'; import ConfirmationModal from './confirmation_modal'; -import SubscribedLanguagesModal from 'flavours/glitch/features/subscribed_languages_modal'; import FocalPointModal from './focal_point_modal'; import DeprecatedSettingsModal from './deprecated_settings_modal'; -import InteractionModal from 'flavours/glitch/features/interaction_modal'; import { OnboardingModal, MuteModal, @@ -29,7 +27,11 @@ import { PinnedAccountsEditor, CompareHistoryModal, FilterModal, + InteractionModal, + SubscribedLanguagesModal, + ClosedRegistrationsModal, } from 'flavours/glitch/features/ui/util/async-components'; +import { Helmet } from 'react-helmet'; const MODAL_COMPONENTS = { 'MEDIA': () => Promise.resolve({ default: MediaModal }), @@ -53,8 +55,9 @@ const MODAL_COMPONENTS = { 'PINNED_ACCOUNTS_EDITOR': PinnedAccountsEditor, 'COMPARE_HISTORY': CompareHistoryModal, 'FILTER': FilterModal, - 'SUBSCRIBED_LANGUAGES': () => Promise.resolve({ default: SubscribedLanguagesModal }), - 'INTERACTION': () => Promise.resolve({ default: InteractionModal }), + 'SUBSCRIBED_LANGUAGES': SubscribedLanguagesModal, + 'INTERACTION': InteractionModal, + 'CLOSED_REGISTRATIONS': ClosedRegistrationsModal, }; export default class ModalRoot extends React.PureComponent { @@ -119,9 +122,15 @@ export default class ModalRoot extends React.PureComponent { return ( <Base backgroundColor={backgroundColor} onClose={this.handleClose} noEsc={props ? props.noEsc : false} ignoreFocus={ignoreFocus}> {visible && ( - <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> - {(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />} - </BundleContainer> + <> + <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> + {(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />} + </BundleContainer> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> + </> )} </Base> ); diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js index 57fbfb285..c3f14ac72 100644 --- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js +++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js @@ -1,17 +1,35 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { NavLink, Link } from 'react-router-dom'; -import { FormattedMessage } from 'react-intl'; -import Icon from 'flavours/glitch/components/icon'; -import { showTrends } from 'flavours/glitch/initial_state'; -import { preferencesLink, relationshipsLink } from 'flavours/glitch/utils/backend_links'; -import NotificationsCounterIcon from './notifications_counter_icon'; -import FollowRequestsNavLink from './follow_requests_nav_link'; +import { defineMessages, injectIntl } from 'react-intl'; +import { Link } from 'react-router-dom'; +import { timelinePreview, showTrends } from 'flavours/glitch/initial_state'; +import ColumnLink from 'flavours/glitch/features/ui/components/column_link'; +import FollowRequestsColumnLink from './follow_requests_column_link'; import ListPanel from './list_panel'; -import TrendsContainer from 'flavours/glitch/features/getting_started/containers/trends_container'; +import NotificationsCounterIcon from './notifications_counter_icon'; import SignInBanner from './sign_in_banner'; +import { preferencesLink, relationshipsLink } from 'flavours/glitch/utils/backend_links'; +import NavigationPortal from 'flavours/glitch/components/navigation_portal'; + +const messages = defineMessages({ + home: { id: 'tabs_bar.home', defaultMessage: 'Home' }, + notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, + explore: { id: 'explore.title', defaultMessage: 'Explore' }, + local: { id: 'tabs_bar.local_timeline', defaultMessage: 'Local' }, + federated: { id: 'tabs_bar.federated_timeline', defaultMessage: 'Federated' }, + direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' }, + favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, + bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, + lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, + preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, + followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' }, + about: { id: 'navigation_bar.about', defaultMessage: 'About' }, + search: { id: 'navigation_bar.search', defaultMessage: 'Search' }, + app_settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' }, +}); -export default class NavigationPanel extends React.Component { +export default @injectIntl +class NavigationPanel extends React.Component { static contextTypes = { router: PropTypes.object.isRequired, @@ -23,54 +41,61 @@ export default class NavigationPanel extends React.Component { }; render() { + const { intl, onOpenSettings } = this.props; const { signedIn } = this.context.identity; - const { onOpenSettings } = this.props; return ( <div className='navigation-panel'> {signedIn && ( <React.Fragment> - <NavLink className='column-link column-link--transparent' to='/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 /> + <ColumnLink transparent to='/home' icon='home' text={intl.formatMessage(messages.home)} /> + <ColumnLink transparent to='/notifications' icon={<NotificationsCounterIcon className='column-link__icon' />} text={intl.formatMessage(messages.notifications)} /> + <FollowRequestsColumnLink /> </React.Fragment> )} - { showTrends && <NavLink className='column-link column-link--transparent' to='/explore' data-preview-title-id='explore.title' data-preview-icon='hashtag'><Icon className='column-link__icon' id='hashtag' fixedWidth /><FormattedMessage id='explore.title' defaultMessage='Explore' /></NavLink> } + {showTrends ? ( + <ColumnLink transparent to='/explore' icon='hashtag' text={intl.formatMessage(messages.explore)} /> + ) : ( + <ColumnLink transparent to='/search' icon='search' text={intl.formatMessage(messages.search)} /> + )} - <NavLink className='column-link column-link--transparent' to='/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='/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> + {(signedIn || timelinePreview) && ( + <> + <ColumnLink transparent to='/public/local' icon='users' text={intl.formatMessage(messages.local)} /> + <ColumnLink transparent exact to='/public' icon='globe' text={intl.formatMessage(messages.federated)} /> + </> + )} {!signedIn && ( - <React.Fragment> + <div className='navigation-panel__sign-in-banner'> <hr /> <SignInBanner /> - </React.Fragment> + </div> )} {signedIn && ( <React.Fragment> - <NavLink className='column-link column-link--transparent' to='/conversations'><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='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink> - <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink> + <ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} /> + <ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} /> + <ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} /> + <ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} /> <ListPanel /> <hr /> - {!!preferencesLink && <a className='column-link column-link--transparent' href={preferencesLink} target='_blank'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>} - <a className='column-link column-link--transparent' href='#' onClick={onOpenSettings}><Icon className='column-link__icon' id='cogs' fixedWidth /><FormattedMessage id='navigation_bar.app_settings' defaultMessage='App settings' /></a> - {!!relationshipsLink && <a className='column-link column-link--transparent' href={relationshipsLink} target='_blank'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>} + {!!preferencesLink && <ColumnLink transparent href={preferencesLink} icon='cog' text={intl.formatMessage(messages.preferences)} />} + <ColumnLink transparent href='#' onClick={onOpenSettings} icon='cogs' text={intl.formatMessage(messages.app_settings)} /> </React.Fragment> )} - {showTrends && ( - <React.Fragment> - <div className='flex-spacer' /> - <TrendsContainer /> - </React.Fragment> - )} + <div className='navigation-panel__legal'> + <hr /> + <ColumnLink transparent to='/about' icon='ellipsis-h' text={intl.formatMessage(messages.about)} /> + </div> + <NavigationPortal /> </div> ); } diff --git a/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.js b/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.js index a935d7422..e8023803f 100644 --- a/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.js +++ b/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.js @@ -1,13 +1,40 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; +import { useDispatch } from 'react-redux'; import { registrationsOpen } from 'flavours/glitch/initial_state'; +import { openModal } from 'flavours/glitch/actions/modal'; -const SignInBanner = () => ( - <div className='sign-in-banner'> - <p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.' /></p> - <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a> - <a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a> - </div> -); +const SignInBanner = () => { + const dispatch = useDispatch(); + + const openClosedRegistrationsModal = useCallback( + () => dispatch(openModal('CLOSED_REGISTRATIONS')), + [dispatch], + ); + + let signupButton; + + if (registrationsOpen) { + signupButton = ( + <a href='/auth/sign_up' className='button button--block button-tertiary'> + <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /> + </a> + ); + } else { + signupButton = ( + <button className='button button--block button-tertiary' onClick={openClosedRegistrationsModal}> + <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /> + </button> + ); + } + + return ( + <div className='sign-in-banner'> + <p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.' /></p> + <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a> + {signupButton} + </div> + ); +}; export default SignInBanner; diff --git a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js deleted file mode 100644 index 9c82fc91d..000000000 --- a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { NavLink, withRouter } from 'react-router-dom'; -import { FormattedMessage, injectIntl } from 'react-intl'; -import { debounce } from 'lodash'; -import { isUserTouching } from 'flavours/glitch/is_mobile'; -import Icon from 'flavours/glitch/components/icon'; -import NotificationsCounterIcon from './notifications_counter_icon'; - -export const links = [ - <NavLink className='tabs-bar__link' to='/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='/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='/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='search' ><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) { - return links.findIndex(link => link.props.to === path); -} - -export function getLink (index) { - return links[index].props.to; -} - -export default @injectIntl -@withRouter -class TabsBar extends React.PureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, - } - - setRef = ref => { - this.node = ref; - } - - handleClick = (e) => { - // Only apply optimization for touch devices, which we assume are slower - // We thus avoid the 250ms delay for non-touch devices and the lag for touch devices - if (isUserTouching()) { - e.preventDefault(); - e.persist(); - - requestAnimationFrame(() => { - const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link')); - const currentTab = tabs.find(tab => tab.classList.contains('active')); - const nextTab = tabs.find(tab => tab.contains(e.target)); - const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)]; - - - if (currentTab !== nextTab) { - if (currentTab) { - currentTab.classList.remove('active'); - } - - const listener = debounce(() => { - nextTab.removeEventListener('transitionend', listener); - this.props.history.push(to); - }, 50); - - nextTab.addEventListener('transitionend', listener); - nextTab.classList.add('active'); - } - }); - } - - } - - render () { - const { intl: { formatMessage } } = this.props; - - return ( - <div className='tabs-bar__wrapper'> - <nav className='tabs-bar' ref={this.setRef}> - {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))} - </nav> - - <div id='tabs-bar__portal' /> - </div> - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index c8cc905e7..3d385eee2 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import LoadingBarContainer from './containers/loading_bar_container'; import ModalContainer from './containers/modal_container'; import { connect } from 'react-redux'; -import { Redirect, withRouter } from 'react-router-dom'; +import { Redirect, Route, withRouter } from 'react-router-dom'; import { layoutFromWindow } from 'flavours/glitch/is_mobile'; import { debounce } from 'lodash'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from 'flavours/glitch/actions/compose'; @@ -15,6 +15,7 @@ import { clearHeight } from 'flavours/glitch/actions/height_cache'; import { changeLayout } from 'flavours/glitch/actions/app'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; +import BundleColumnError from './components/bundle_column_error'; import UploadArea from './components/upload_area'; import PermaLink from 'flavours/glitch/components/permalink'; import ColumnsAreaContainer from './containers/columns_area_container'; @@ -39,7 +40,6 @@ import { HashtagTimeline, Notifications, FollowRequests, - GenericNotFound, FavouritedStatuses, BookmarkedStatuses, ListTimeline, @@ -52,12 +52,15 @@ import { Directory, Explore, FollowRecommendations, + About, + PrivacyPolicy, } from './util/async-components'; import { HotKeys } from 'react-hotkeys'; -import { me, title } from 'flavours/glitch/initial_state'; +import initialState, { me, owner, singleUserMode, showTrends } from '../../initial_state'; import { closeOnboarding, INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; import { Helmet } from 'react-helmet'; +import Header from './components/header'; // Dummy import, to make sure that <Status /> ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. @@ -156,7 +159,7 @@ class SwitchingColumnsArea extends React.PureComponent { setRef = c => { if (c) { - this.node = c.getWrappedInstance(); + this.node = c; } } @@ -172,8 +175,12 @@ class SwitchingColumnsArea extends React.PureComponent { } else { redirect = <Redirect from='/' to='/getting-started' exact />; } - } else { + } else if (singleUserMode && owner && initialState?.accounts[owner]) { + redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />; + } else if (showTrends) { redirect = <Redirect from='/' to='/explore' exact />; + } else { + redirect = <Redirect from='/' to='/about' exact />; } return ( @@ -183,6 +190,8 @@ class SwitchingColumnsArea extends React.PureComponent { <WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> + <WrappedRoute path='/about' component={About} content={children} /> + <WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} /> <WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} /> <WrappedRoute path={['/public', '/timelines/public']} exact component={PublicTimeline} content={children} /> @@ -202,9 +211,10 @@ class SwitchingColumnsArea extends React.PureComponent { <WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} /> <WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} /> + <WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} /> <WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} /> - <WrappedRoute path={['/@:acct/followers', '/accounts/:id/followers']} component={Followers} content={children} /> - <WrappedRoute path={['/@:acct/following', '/accounts/:id/following']} component={Following} content={children} /> + <WrappedRoute path={['/accounts/:id/followers', '/users/:acct/followers', '/@:acct/followers']} component={Followers} content={children} /> + <WrappedRoute path={['/accounts/:id/following', '/users/:acct/following', '/@:acct/following']} component={Following} content={children} /> <WrappedRoute path={['/@:acct/media', '/accounts/:id/media']} component={AccountGallery} content={children} /> <WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} /> <WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} /> @@ -224,7 +234,7 @@ class SwitchingColumnsArea extends React.PureComponent { <WrappedRoute path='/lists' component={Lists} content={children} /> <WrappedRoute path='/getting-started-misc' component={GettingStartedMisc} content={children} /> - <WrappedRoute component={GenericNotFound} content={children} /> + <Route component={BundleColumnError} /> </WrappedSwitch> </ColumnsAreaContainer> ); @@ -652,6 +662,9 @@ class UI extends React.Component { )}} /> </div>)} + + <Header /> + <SwitchingColumnsArea location={location} mobile={layout === 'mobile' || layout === 'single-column'} navbarUnder={navbarUnder}> {children} </SwitchingColumnsArea> @@ -661,10 +674,6 @@ class UI extends React.Component { <LoadingBarContainer className='loading-bar' /> <ModalContainer /> <UploadArea active={draggingOver} onClose={this.closeUploadModal} /> - - <Helmet> - <title>{title}</title> - </Helmet> </div> </HotKeys> ); diff --git a/app/javascript/flavours/glitch/features/ui/util/async-components.js b/app/javascript/flavours/glitch/features/ui/util/async-components.js index eef3a941d..025b22e61 100644 --- a/app/javascript/flavours/glitch/features/ui/util/async-components.js +++ b/app/javascript/flavours/glitch/features/ui/util/async-components.js @@ -181,3 +181,23 @@ export function FilterModal () { export function Explore () { return import(/* webpackChunkName: "flavours/glitch/async/explore" */'flavours/glitch/features/explore'); } + +export function InteractionModal () { + return import(/*webpackChunkName: "flavours/glitch/async/modals/interaction_modal" */'flavours/glitch/features/interaction_modal'); +} + +export function SubscribedLanguagesModal () { + return import(/*webpackChunkName: "flavours/glitch/async/modals/subscribed_languages_modal" */'flavours/glitch/features/subscribed_languages_modal'); +} + +export function ClosedRegistrationsModal () { + return import(/*webpackChunkName: "flavours/glitch/async/modals/closed_registrations_modal" */'flavours/glitch/features/closed_registrations_modal'); +} + +export function About () { + return import(/*webpackChunkName: "features/glitch/async/about" */'flavours/glitch/features/about'); +} + +export function PrivacyPolicy () { + return import(/*webpackChunkName: "features/glitch/async/privacy_policy" */'flavours/glitch/features/privacy_policy'); +} diff --git a/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.js b/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.js index e36c512f3..8946c8252 100644 --- a/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.js +++ b/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Switch, Route } from 'react-router-dom'; - +import StackTrace from 'stacktrace-js'; import ColumnLoading from 'flavours/glitch/features/ui/components/column_loading'; import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; import BundleContainer from 'flavours/glitch/features/ui/containers/bundle_container'; @@ -42,8 +42,38 @@ export class WrappedRoute extends React.Component { componentParams: {}, }; + static getDerivedStateFromError () { + return { + hasError: true, + }; + }; + + state = { + hasError: false, + stacktrace: '', + }; + + componentDidCatch (error) { + StackTrace.fromError(error).then(stackframes => { + this.setState({ stacktrace: error.toString() + '\n' + stackframes.map(frame => frame.toString()).join('\n') }); + }).catch(err => { + console.error(err); + }); + } + renderComponent = ({ match }) => { const { component, content, multiColumn, componentParams } = this.props; + const { hasError, stacktrace } = this.state; + + if (hasError) { + return ( + <BundleColumnError + stacktrace={stacktrace} + multiColumn={multiColumn} + errorType='error' + /> + ); + } return ( <BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}> @@ -53,11 +83,13 @@ export class WrappedRoute extends React.Component { } renderLoading = () => { - return <ColumnLoading />; + const { multiColumn } = this.props; + + return <ColumnLoading multiColumn={multiColumn} />; } renderError = (props) => { - return <BundleColumnError {...props} />; + return <BundleColumnError {...props} errorType='network' />; } render () { diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js index 0f2484ba3..ce6bf920c 100644 --- a/app/javascript/flavours/glitch/initial_state.js +++ b/app/javascript/flavours/glitch/initial_state.js @@ -1,5 +1,95 @@ +// @ts-check + +/** + * @typedef Emoji + * @property {string} shortcode + * @property {string} static_url + * @property {string} url + */ + +/** + * @typedef AccountField + * @property {string} name + * @property {string} value + * @property {string} verified_at + */ + +/** + * @typedef Account + * @property {string} acct + * @property {string} avatar + * @property {string} avatar_static + * @property {boolean} bot + * @property {string} created_at + * @property {boolean=} discoverable + * @property {string} display_name + * @property {Emoji[]} emojis + * @property {AccountField[]} fields + * @property {number} followers_count + * @property {number} following_count + * @property {boolean} group + * @property {string} header + * @property {string} header_static + * @property {string} id + * @property {string=} last_status_at + * @property {boolean} locked + * @property {string} note + * @property {number} statuses_count + * @property {string} url + * @property {string} username + */ + +/** + * @typedef {[code: string, name: string, localName: string]} InitialStateLanguage + */ + +/** + * @typedef InitialStateMeta + * @property {string} access_token + * @property {boolean=} advanced_layout + * @property {boolean} auto_play_gif + * @property {boolean} activity_api_enabled + * @property {string} admin + * @property {boolean=} boost_modal + * @property {boolean} crop_images + * @property {boolean=} delete_modal + * @property {boolean=} disable_swiping + * @property {boolean} display_media + * @property {string} domain + * @property {boolean=} expand_spoilers + * @property {boolean} limited_federation_mode + * @property {string} locale + * @property {string | null} mascot + * @property {string=} me + * @property {string=} owner + * @property {boolean} profile_directory + * @property {boolean} registrations_open + * @property {boolean} reduce_motion + * @property {string} repository + * @property {boolean} search_enabled + * @property {boolean} single_user_mode + * @property {string} source_url + * @property {string} streaming_api_base_url + * @property {boolean} timeline_preview + * @property {string} title + * @property {boolean} trends + * @property {boolean} unfollow_modal + * @property {boolean} use_blurhash + * @property {boolean=} use_pending_items + * @property {string} version + * @property {object} local_settings + */ + +/** + * @typedef InitialState + * @property {Record<string, Account>} accounts + * @property {InitialStateLanguage[]} languages + * @property {InitialStateMeta} meta + */ + const element = document.getElementById('initial-state'); -const initialState = element && JSON.parse(element.textContent); +/** @type {InitialState | undefined} */ +const initialState = element?.textContent && JSON.parse(element.textContent); // Glitch-soc-specific “local settings” try { @@ -8,35 +98,45 @@ try { initialState.local_settings = {}; } -const getMeta = (prop) => initialState && initialState.meta && initialState.meta[prop]; +/** + * @template {keyof InitialStateMeta} K + * @param {K} prop + * @returns {InitialStateMeta[K] | undefined} + */ +const getMeta = (prop) => initialState?.meta && initialState.meta[prop]; -export const domain = getMeta('domain'); -export const reduceMotion = getMeta('reduce_motion'); +export const activityApiEnabled = getMeta('activity_api_enabled'); export const autoPlayGif = getMeta('auto_play_gif'); -export const displayMedia = getMeta('display_media'); -export const expandSpoilers = getMeta('expand_spoilers'); -export const unfollowModal = getMeta('unfollow_modal'); export const boostModal = getMeta('boost_modal'); +export const cropImages = getMeta('crop_images'); 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 disableSwiping = getMeta('disable_swiping'); +export const displayMedia = getMeta('display_media'); +export const domain = getMeta('domain'); +export const expandSpoilers = getMeta('expand_spoilers'); +export const forceSingleColumn = !getMeta('advanced_layout'); export const limitedFederationMode = getMeta('limited_federation_mode'); +export const mascot = getMeta('mascot'); +export const me = getMeta('me'); +export const owner = getMeta('owner'); +export const profile_directory = getMeta('profile_directory'); +export const reduceMotion = getMeta('reduce_motion'); export const registrationsOpen = getMeta('registrations_open'); export const repository = getMeta('repository'); +export const searchEnabled = getMeta('search_enabled'); +export const showTrends = getMeta('trends'); +export const singleUserMode = getMeta('single_user_mode'); export const source_url = getMeta('source_url'); -export const version = getMeta('version'); -export const mascot = getMeta('mascot'); -export const profile_directory = getMeta('profile_directory'); -export const forceSingleColumn = !getMeta('advanced_layout'); +export const timelinePreview = getMeta('timeline_preview'); +export const title = getMeta('title'); +export const unfollowModal = getMeta('unfollow_modal'); export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); -export const showTrends = getMeta('trends'); -export const title = getMeta('title'); -export const disableSwiping = getMeta('disable_swiping'); -export const languages = initialState && initialState.languages; +export const version = getMeta('version'); +export const languages = initialState?.languages; // Glitch-soc-specific settings +export const maxChars = (initialState && initialState.max_toot_chars) || 500; export const favouriteModal = getMeta('favourite_modal'); export const pollLimits = (initialState && initialState.poll_limits); export const defaultContentType = getMeta('default_content_type'); diff --git a/app/javascript/flavours/glitch/is_mobile.js b/app/javascript/flavours/glitch/is_mobile.js index 0d5663098..31944d89b 100644 --- a/app/javascript/flavours/glitch/is_mobile.js +++ b/app/javascript/flavours/glitch/is_mobile.js @@ -1,10 +1,20 @@ +// @ts-check + import { supportsPassiveEvents } from 'detect-passive-events'; import { forceSingleColumn } from 'flavours/glitch/initial_state'; const LAYOUT_BREAKPOINT = 630; +/** + * @param {number} width + * @returns {boolean} + */ export const isMobile = width => width <= LAYOUT_BREAKPOINT; +/** + * @param {string} layout_local_setting + * @returns {string} + */ export const layoutFromWindow = (layout_local_setting) => { switch (layout_local_setting) { case 'multiple': @@ -28,11 +38,13 @@ export const layoutFromWindow = (layout_local_setting) => { const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + let userTouching = false; -let listenerOptions = supportsPassiveEvents ? { passive: true } : false; const touchListener = () => { userTouching = true; + window.removeEventListener('touchstart', touchListener, listenerOptions); }; diff --git a/app/javascript/flavours/glitch/load_polyfills.js b/app/javascript/flavours/glitch/load_polyfills.js index 73eedc9dc..cc5bcd18f 100644 --- a/app/javascript/flavours/glitch/load_polyfills.js +++ b/app/javascript/flavours/glitch/load_polyfills.js @@ -26,6 +26,7 @@ function loadPolyfills() { // Edge does not have requestIdleCallback and object-fit CSS property. // This avoids shipping them all the polyfills. const needsExtraPolyfills = !( + window.AbortController && window.IntersectionObserver && window.IntersectionObserverEntry && 'isIntersecting' in IntersectionObserverEntry.prototype && diff --git a/app/javascript/flavours/glitch/locales/ja.js b/app/javascript/flavours/glitch/locales/ja.js index 0ca5e5fc7..52aeed3d6 100644 --- a/app/javascript/flavours/glitch/locales/ja.js +++ b/app/javascript/flavours/glitch/locales/ja.js @@ -8,6 +8,7 @@ const messages = { 'layout.single': 'モバイル', 'navigation_bar.app_settings': 'アプリ設定', 'navigation_bar.featured_users': '紹介しているアカウント', + 'navigation_bar.misc': 'その他', 'getting_started.onboarding': '解説を表示', 'onboarding.page_one.federation': '{domain}はMastodonのインスタンスです。Mastodonとは、独立したサーバが連携して作るソーシャルネットワークです。これらのサーバーをインスタンスと呼びます。', 'onboarding.page_one.welcome': '{domain}へようこそ!', diff --git a/app/javascript/flavours/glitch/main.js b/app/javascript/flavours/glitch/main.js index ecb19ef54..f1e10df34 100644 --- a/app/javascript/flavours/glitch/main.js +++ b/app/javascript/flavours/glitch/main.js @@ -12,14 +12,6 @@ const perf = require('flavours/glitch/performance'); function main() { perf.start('main()'); - if (window.history && history.replaceState) { - const { pathname, search, hash } = window.location; - const path = pathname + search + hash; - if (!(/^\/web($|\/)/).test(path)) { - history.replaceState(null, document.title, `/web${path}`); - } - } - return ready(async () => { const mountNode = document.getElementById('mastodon'); const props = JSON.parse(mountNode.getAttribute('data-props')); diff --git a/app/javascript/flavours/glitch/packs/about.js b/app/javascript/flavours/glitch/packs/about.js deleted file mode 100644 index ef17fdea4..000000000 --- a/app/javascript/flavours/glitch/packs/about.js +++ /dev/null @@ -1,23 +0,0 @@ -import 'packs/public-path'; -import loadPolyfills from 'flavours/glitch/load_polyfills'; - -function loaded() { - const TimelineContainer = require('flavours/glitch/containers/timeline_container').default; - const React = require('react'); - const ReactDOM = require('react-dom'); - const mountNode = document.getElementById('mastodon-timeline'); - - if (mountNode !== null) { - const props = JSON.parse(mountNode.getAttribute('data-props')); - ReactDOM.render(<TimelineContainer {...props} />, mountNode); - } -} - -function main() { - const ready = require('flavours/glitch/ready').default; - ready(loaded); -} - -loadPolyfills().then(main).catch(error => { - console.error(error); -}); diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js index ae1899638..843fd5163 100644 --- a/app/javascript/flavours/glitch/packs/public.js +++ b/app/javascript/flavours/glitch/packs/public.js @@ -12,7 +12,6 @@ function main() { const { messages } = getLocale(); const React = require('react'); const ReactDOM = require('react-dom'); - const Rellax = require('rellax'); const { createBrowserHistory } = require('history'); const scrollToDetailedStatus = () => { @@ -90,12 +89,6 @@ function main() { scrollToDetailedStatus(); } - const parallaxComponents = document.querySelectorAll('.parallax'); - - if (parallaxComponents.length > 0 ) { - new Rellax('.parallax', { speed: -1 }); - } - delegate(document, '#registration_user_password_confirmation,#registration_user_password', 'input', () => { const password = document.getElementById('registration_user_password'); const confirmation = document.getElementById('registration_user_password_confirmation'); @@ -146,8 +139,31 @@ function main() { }); }); + const toggleSidebar = () => { + const sidebar = document.querySelector('.sidebar ul'); + const toggleButton = document.querySelector('.sidebar__toggle__icon'); + + if (sidebar.classList.contains('visible')) { + document.body.style.overflow = null; + toggleButton.setAttribute('aria-expanded', false); + } else { + document.body.style.overflow = 'hidden'; + toggleButton.setAttribute('aria-expanded', true); + } + + toggleButton.classList.toggle('active'); + sidebar.classList.toggle('visible'); + }; + delegate(document, '.sidebar__toggle__icon', 'click', () => { - document.querySelector('.sidebar ul').classList.toggle('visible'); + toggleSidebar(); + }); + + delegate(document, '.sidebar__toggle__icon', 'keydown', e => { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault(); + toggleSidebar(); + } }); // Empty the honeypot fields in JS in case something like an extension diff --git a/app/javascript/flavours/glitch/packs/settings.js b/app/javascript/flavours/glitch/packs/settings.js index 4c85f6556..31c88b2b5 100644 --- a/app/javascript/flavours/glitch/packs/settings.js +++ b/app/javascript/flavours/glitch/packs/settings.js @@ -7,8 +7,31 @@ import 'cocoon-js-vanilla'; function main() { const { delegate } = require('@rails/ujs'); + const toggleSidebar = () => { + const sidebar = document.querySelector('.sidebar ul'); + const toggleButton = document.querySelector('.sidebar__toggle__icon'); + + if (sidebar.classList.contains('visible')) { + document.body.style.overflow = null; + toggleButton.setAttribute('aria-expanded', false); + } else { + document.body.style.overflow = 'hidden'; + toggleButton.setAttribute('aria-expanded', true); + } + + toggleButton.classList.toggle('active'); + sidebar.classList.toggle('visible'); + }; + delegate(document, '.sidebar__toggle__icon', 'click', () => { - document.querySelector('.sidebar ul').classList.toggle('visible'); + toggleSidebar(); + }); + + delegate(document, '.sidebar__toggle__icon', 'keydown', e => { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault(); + toggleSidebar(); + } }); } diff --git a/app/javascript/flavours/glitch/reducers/accounts_map.js b/app/javascript/flavours/glitch/reducers/accounts_map.js index e0d42e9cd..53e08c8fb 100644 --- a/app/javascript/flavours/glitch/reducers/accounts_map.js +++ b/app/javascript/flavours/glitch/reducers/accounts_map.js @@ -1,14 +1,16 @@ import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer'; import { Map as ImmutableMap } from 'immutable'; +export const normalizeForLookup = str => str.toLowerCase(); + const initialState = ImmutableMap(); export default function accountsMap(state = initialState, action) { switch(action.type) { case ACCOUNT_IMPORT: - return state.set(action.account.acct, action.account.id); + return state.set(normalizeForLookup(action.account.acct), action.account.id); case ACCOUNTS_IMPORT: - return state.withMutations(map => action.accounts.forEach(account => map.set(account.acct, account.id))); + return state.withMutations(map => action.accounts.forEach(account => map.set(normalizeForLookup(account.acct), account.id))); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 035e9f564..b739456da 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -15,6 +15,7 @@ import { COMPOSE_UPLOAD_FAIL, COMPOSE_UPLOAD_UNDO, COMPOSE_UPLOAD_PROGRESS, + COMPOSE_UPLOAD_PROCESSING, THUMBNAIL_UPLOAD_REQUEST, THUMBNAIL_UPLOAD_SUCCESS, THUMBNAIL_UPLOAD_FAIL, @@ -223,6 +224,7 @@ function appendMedia(state, media, file) { } map.update('media_attachments', list => list.push(media)); map.set('is_uploading', false); + map.set('is_processing', false); map.set('resetFileKey', Math.floor((Math.random() * 0x10000))); map.set('idempotencyKey', uuid()); map.update('pending_media_attachments', n => n - 1); @@ -465,10 +467,12 @@ export default function compose(state = initialState, action) { return state.set('is_changing_upload', false); case COMPOSE_UPLOAD_REQUEST: return state.set('is_uploading', true).update('pending_media_attachments', n => n + 1); + case COMPOSE_UPLOAD_PROCESSING: + return state.set('is_processing', true); case COMPOSE_UPLOAD_SUCCESS: return appendMedia(state, fromJS(action.media), action.file); case COMPOSE_UPLOAD_FAIL: - return state.set('is_uploading', false).update('pending_media_attachments', n => n - 1); + return state.set('is_uploading', false).set('is_processing', false).update('pending_media_attachments', n => n - 1); case COMPOSE_UPLOAD_UNDO: return removeMedia(state, action.media_id); case COMPOSE_UPLOAD_PROGRESS: diff --git a/app/javascript/flavours/glitch/reducers/server.js b/app/javascript/flavours/glitch/reducers/server.js index 977574148..cc5798fb3 100644 --- a/app/javascript/flavours/glitch/reducers/server.js +++ b/app/javascript/flavours/glitch/reducers/server.js @@ -1,18 +1,52 @@ -import { SERVER_FETCH_REQUEST, SERVER_FETCH_SUCCESS, SERVER_FETCH_FAIL } from 'flavours/glitch/actions/server'; -import { Map as ImmutableMap, fromJS } from 'immutable'; +import { + SERVER_FETCH_REQUEST, + SERVER_FETCH_SUCCESS, + SERVER_FETCH_FAIL, + EXTENDED_DESCRIPTION_REQUEST, + EXTENDED_DESCRIPTION_SUCCESS, + EXTENDED_DESCRIPTION_FAIL, + SERVER_DOMAIN_BLOCKS_FETCH_REQUEST, + SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS, + SERVER_DOMAIN_BLOCKS_FETCH_FAIL, +} from 'flavours/glitch/actions/server'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; const initialState = ImmutableMap({ - isLoading: true, + server: ImmutableMap({ + isLoading: true, + }), + + extendedDescription: ImmutableMap({ + isLoading: true, + }), + + domainBlocks: ImmutableMap({ + isLoading: true, + isAvailable: true, + items: ImmutableList(), + }), }); export default function server(state = initialState, action) { switch (action.type) { case SERVER_FETCH_REQUEST: - return state.set('isLoading', true); + return state.setIn(['server', 'isLoading'], true); case SERVER_FETCH_SUCCESS: - return fromJS(action.server).set('isLoading', false); + return state.set('server', fromJS(action.server)).setIn(['server', 'isLoading'], false); case SERVER_FETCH_FAIL: - return state.set('isLoading', false); + return state.setIn(['server', 'isLoading'], false); + case EXTENDED_DESCRIPTION_REQUEST: + return state.setIn(['extendedDescription', 'isLoading'], true); + case EXTENDED_DESCRIPTION_SUCCESS: + return state.set('extendedDescription', fromJS(action.description)).setIn(['extendedDescription', 'isLoading'], false); + case EXTENDED_DESCRIPTION_FAIL: + return state.setIn(['extendedDescription', 'isLoading'], false); + case SERVER_DOMAIN_BLOCKS_FETCH_REQUEST: + return state.setIn(['domainBlocks', 'isLoading'], true); + case SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS: + return state.setIn(['domainBlocks', 'items'], fromJS(action.blocks)).setIn(['domainBlocks', 'isLoading'], false).setIn(['domainBlocks', 'isAvailable'], action.isAvailable); + case SERVER_DOMAIN_BLOCKS_FETCH_FAIL: + return state.setIn(['domainBlocks', 'isLoading'], false); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js index 333e4b45c..b47155c5f 100644 --- a/app/javascript/flavours/glitch/reducers/statuses.js +++ b/app/javascript/flavours/glitch/reducers/statuses.js @@ -13,6 +13,8 @@ import { STATUS_REVEAL, STATUS_HIDE, STATUS_COLLAPSE, + STATUS_FETCH_REQUEST, + STATUS_FETCH_FAIL, } from 'flavours/glitch/actions/statuses'; import { TIMELINE_DELETE, @@ -37,6 +39,10 @@ const initialState = ImmutableMap(); export default function statuses(state = initialState, action) { switch(action.type) { + case STATUS_FETCH_REQUEST: + return state.setIn([action.id, 'isLoading'], true); + case STATUS_FETCH_FAIL: + return state.delete(action.id); case STATUS_IMPORT: return importStatus(state, action.status); case STATUSES_IMPORT: diff --git a/app/javascript/flavours/glitch/reducers/user_lists.js b/app/javascript/flavours/glitch/reducers/user_lists.js index bfddbd246..0a75e85c1 100644 --- a/app/javascript/flavours/glitch/reducers/user_lists.js +++ b/app/javascript/flavours/glitch/reducers/user_lists.js @@ -51,7 +51,12 @@ import { DIRECTORY_EXPAND_SUCCESS, DIRECTORY_EXPAND_FAIL, } from 'flavours/glitch/actions/directory'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { + FEATURED_TAGS_FETCH_REQUEST, + FEATURED_TAGS_FETCH_SUCCESS, + FEATURED_TAGS_FETCH_FAIL, +} from 'flavours/glitch/actions/featured_tags'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; const initialListState = ImmutableMap({ next: null, @@ -67,6 +72,7 @@ const initialState = ImmutableMap({ follow_requests: initialListState, blocks: initialListState, mutes: initialListState, + featured_tags: initialListState, }); const normalizeList = (state, path, accounts, next) => { @@ -89,6 +95,18 @@ const normalizeFollowRequest = (state, notification) => { }); }; +const normalizeFeaturedTag = (featuredTags, accountId) => { + const normalizeFeaturedTag = { ...featuredTags, accountId: accountId }; + return fromJS(normalizeFeaturedTag); +}; + +const normalizeFeaturedTags = (state, path, featuredTags, accountId) => { + return state.setIn(path, ImmutableMap({ + items: ImmutableList(featuredTags.map(featuredTag => normalizeFeaturedTag(featuredTag, accountId)).sort((a, b) => b.get('statuses_count') - a.get('statuses_count'))), + isLoading: false, + })); +}; + export default function userLists(state = initialState, action) { switch(action.type) { case FOLLOWERS_FETCH_SUCCESS: @@ -160,6 +178,12 @@ export default function userLists(state = initialState, action) { case DIRECTORY_FETCH_FAIL: case DIRECTORY_EXPAND_FAIL: return state.setIn(['directory', 'isLoading'], false); + case FEATURED_TAGS_FETCH_SUCCESS: + return normalizeFeaturedTags(state, ['featured_tags', action.id], action.tags, action.id); + case FEATURED_TAGS_FETCH_REQUEST: + return state.setIn(['featured_tags', action.id, 'isLoading'], true); + case FEATURED_TAGS_FETCH_FAIL: + return state.setIn(['featured_tags', action.id, 'isLoading'], false); default: return state; } diff --git a/app/javascript/flavours/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js index 8e6e40d24..df46b58a8 100644 --- a/app/javascript/flavours/glitch/selectors/index.js +++ b/app/javascript/flavours/glitch/selectors/index.js @@ -42,7 +42,7 @@ export const makeGetStatus = () => { ], (statusBase, statusReblog, accountBase, accountReblog, filters) => { - if (!statusBase) { + if (!statusBase || statusBase.get('isLoading')) { return null; } diff --git a/app/javascript/flavours/glitch/settings.js b/app/javascript/flavours/glitch/settings.js index 7643a508e..46cfadfa3 100644 --- a/app/javascript/flavours/glitch/settings.js +++ b/app/javascript/flavours/glitch/settings.js @@ -45,3 +45,4 @@ export default class Settings { export const pushNotificationsSetting = new Settings('mastodon_push_notification_data'); export const tagHistory = new Settings('mastodon_tag_history'); +export const bannerSettings = new Settings('mastodon_banner_settings'); diff --git a/app/javascript/flavours/glitch/styles/about.scss b/app/javascript/flavours/glitch/styles/about.scss index 1843129a0..0183c43d5 100644 --- a/app/javascript/flavours/glitch/styles/about.scss +++ b/app/javascript/flavours/glitch/styles/about.scss @@ -1,7 +1,5 @@ $maximum-width: 1235px; $fluid-breakpoint: $maximum-width + 20px; -$column-breakpoint: 700px; -$small-breakpoint: 960px; .container { box-sizing: border-box; @@ -15,894 +13,44 @@ $small-breakpoint: 960px; } } -.rich-formatting { - font-family: $font-sans-serif, sans-serif; - font-size: 14px; - font-weight: 400; - line-height: 1.7; - word-wrap: break-word; - color: $darker-text-color; - - a { - color: $highlight-text-color; - text-decoration: underline; - - &:hover, - &:focus, - &:active { - text-decoration: none; - } - } - - p, - li { - color: $darker-text-color; - } - - p { - margin-top: 0; - margin-bottom: .85em; - - &:last-child { - margin-bottom: 0; - } - } - - strong { - font-weight: 700; - color: $secondary-text-color; - } - - em { - font-style: italic; - color: $secondary-text-color; - } - - code { - font-size: 0.85em; - background: darken($ui-base-color, 8%); - border-radius: 4px; - padding: 0.2em 0.3em; - } - - h1, - h2, - h3, - h4, - h5, - h6 { - font-family: $font-display, sans-serif; - margin-top: 1.275em; - margin-bottom: .85em; - font-weight: 500; - color: $secondary-text-color; - } - - h1 { - font-size: 2em; - } - - h2 { - font-size: 1.75em; - } - - h3 { - font-size: 1.5em; - } - - h4 { - font-size: 1.25em; - } - - h5, - h6 { - font-size: 1em; - } - - ul { - list-style: disc; - } - - ol { - list-style: decimal; - } - - ul, - ol { - margin: 0; - padding: 0; - padding-left: 2em; - margin-bottom: 0.85em; - - &[type='a'] { - list-style-type: lower-alpha; - } - - &[type='i'] { - list-style-type: lower-roman; - } - } - - hr { - width: 100%; - height: 0; - border: 0; - border-bottom: 1px solid lighten($ui-base-color, 4%); - margin: 1.7em 0; - - &.spacer { - height: 1px; - border: 0; - } - } - - table { - width: 100%; - border-collapse: collapse; - break-inside: auto; - margin-top: 24px; - margin-bottom: 32px; - - thead tr, - tbody tr { - border-bottom: 1px solid lighten($ui-base-color, 4%); - font-size: 1em; - line-height: 1.625; - font-weight: 400; - text-align: left; - color: $darker-text-color; - } - - thead tr { - border-bottom-width: 2px; - line-height: 1.5; - font-weight: 500; - color: $dark-text-color; - } - - th, - td { - padding: 8px; - align-self: start; - align-items: start; - word-break: break-all; - - &.nowrap { - width: 25%; - position: relative; - - &::before { - content: ' '; - visibility: hidden; - } - - span { - position: absolute; - left: 8px; - right: 8px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - } - } - - & > :first-child { - margin-top: 0; - } +.brand { + position: relative; + text-decoration: none; } -.information-board { - background: darken($ui-base-color, 4%); - padding: 20px 0; - - .container-alt { - position: relative; - padding-right: 280px + 15px; - } - - &__sections { - display: flex; - justify-content: space-between; - flex-wrap: wrap; - } - - &__section { - flex: 1 0 0; - font-family: $font-sans-serif, sans-serif; - font-size: 16px; - line-height: 28px; - color: $primary-text-color; - text-align: right; - padding: 10px 15px; - - span, - strong { - display: block; - } - - span { - &:last-child { - color: $secondary-text-color; - } - } - - strong { - font-family: $font-display, sans-serif; - font-weight: 500; - font-size: 32px; - line-height: 48px; - } - - @media screen and (max-width: $column-breakpoint) { - text-align: center; - } - } - - .panel { - position: absolute; - width: 280px; - box-sizing: border-box; - background: darken($ui-base-color, 8%); - padding: 20px; - padding-top: 10px; - border-radius: 4px 4px 0 0; - right: 0; - bottom: -40px; - - .panel-header { - font-family: $font-display, sans-serif; - font-size: 14px; - line-height: 24px; - font-weight: 500; - color: $darker-text-color; - padding-bottom: 5px; - margin-bottom: 15px; - border-bottom: 1px solid lighten($ui-base-color, 4%); - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - - a, - span { - font-weight: 400; - color: darken($darker-text-color, 10%); - } - - a { - text-decoration: none; - } - } - } - - .owner { - text-align: center; - - .avatar { - width: 80px; - height: 80px; - @include avatar-size(80px); - margin: 0 auto; - margin-bottom: 15px; - - img { - display: block; - width: 80px; - height: 80px; - border-radius: 48px; - @include avatar-radius(); - } - } - - .name { - font-size: 14px; - - a { - display: block; - color: $primary-text-color; - text-decoration: none; - - &:hover { - .display_name { - text-decoration: underline; - } - } - } - - .username { - display: block; - color: $darker-text-color; - } - } - } -} +.rules-list { + font-size: 15px; + line-height: 22px; + color: $primary-text-color; + counter-reset: list-counter; -.landing-page { - p, li { - font-family: $font-sans-serif, sans-serif; - font-size: 16px; - font-weight: 400; - line-height: 30px; - margin-bottom: 12px; - color: $darker-text-color; - - a { - color: $highlight-text-color; - text-decoration: underline; - } - } - - em { - display: inline; - margin: 0; - padding: 0; - font-weight: 700; - background: transparent; - font-family: inherit; - font-size: inherit; - line-height: inherit; - color: lighten($darker-text-color, 10%); - } - - h1 { - font-family: $font-display, sans-serif; - font-size: 26px; - line-height: 30px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; - - small { - font-family: $font-sans-serif, sans-serif; - display: block; - font-size: 18px; - font-weight: 400; - color: lighten($darker-text-color, 10%); - } - } - - h2 { - font-family: $font-display, sans-serif; - font-size: 22px; - line-height: 26px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; - } - - h3 { - font-family: $font-display, sans-serif; - font-size: 18px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; - } - - h4 { - font-family: $font-display, sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; - } - - h5 { - font-family: $font-display, sans-serif; - font-size: 14px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; - } - - h6 { - font-family: $font-display, sans-serif; - font-size: 12px; - line-height: 24px; + position: relative; + border-bottom: 1px solid lighten($ui-base-color, 8%); + padding: 1em 1.75em; + padding-left: 3em; font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; - } - - ul, - ol { - margin-left: 20px; - - &[type='a'] { - list-style-type: lower-alpha; - } - - &[type='i'] { - list-style-type: lower-roman; - } - } - - ul { - list-style: disc; - } - - ol { - list-style: decimal; - } - - li > ol, - li > ul { - margin-top: 6px; - } - - hr { - width: 100%; - height: 0; - border: 0; - border-bottom: 1px solid rgba($ui-base-lighter-color, .6); - margin: 20px 0; - - &.spacer { - height: 1px; - border: 0; - } - } - - &__information, - &__forms { - padding: 20px; - } - - &__call-to-action { - background: $ui-base-color; - border-radius: 4px; - padding: 25px 40px; - overflow: hidden; - box-sizing: border-box; - - .row { - width: 100%; - display: flex; - flex-direction: row-reverse; - flex-wrap: nowrap; - justify-content: space-between; - align-items: center; - } - - .row__information-board { - display: flex; - justify-content: flex-end; - align-items: flex-end; - - .information-board__section { - flex: 1 0 auto; - padding: 0 10px; - } - - @media screen and (max-width: $no-gap-breakpoint) { - width: 100%; - justify-content: space-between; - } - } - - .row__mascot { - flex: 1; - margin: 10px -50px 0 0; - - @media screen and (max-width: $no-gap-breakpoint) { - display: none; - } - } - } - - &__logo { - margin-right: 20px; - - img { - height: 50px; - width: auto; - mix-blend-mode: lighten; - } - } - - &__information { - padding: 45px 40px; - margin-bottom: 10px; - - &:last-child { - margin-bottom: 0; - } - - strong { + counter-increment: list-counter; + + &::before { + content: counter(list-counter); + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + background: $highlight-text-color; + color: $ui-base-color; + border-radius: 50%; + width: 4ch; + height: 4ch; font-weight: 500; - color: lighten($darker-text-color, 10%); - } - - .account { - border-bottom: 0; - padding: 0; - - &__display-name { - align-items: center; - display: flex; - margin-right: 5px; - } - - div.account__display-name { - &:hover { - .display-name strong { - text-decoration: none; - } - } - - .account__avatar { - cursor: default; - } - } - - &__avatar-wrapper { - margin-left: 0; - flex: 0 0 auto; - } - - .display-name { - font-size: 15px; - - &__account { - font-size: 14px; - } - } - } - - @media screen and (max-width: $small-breakpoint) { - .contact { - margin-top: 30px; - } - } - - @media screen and (max-width: $column-breakpoint) { - padding: 25px 20px; - } - } - - &__information, - &__forms, - #mastodon-timeline { - box-sizing: border-box; - background: $ui-base-color; - border-radius: 4px; - box-shadow: 0 0 6px rgba($black, 0.1); - } - - &__mascot { - height: 104px; - position: relative; - left: -40px; - bottom: 25px; - - img { - height: 190px; - width: auto; - } - } - - &__short-description { - .row { display: flex; - flex-wrap: wrap; + justify-content: center; align-items: center; - margin-bottom: 40px; - } - - @media screen and (max-width: $column-breakpoint) { - .row { - margin-bottom: 20px; - } - } - - p a { - color: $secondary-text-color; - } - - h1 { - font-weight: 500; - color: $primary-text-color; - margin-bottom: 0; - - small { - color: $darker-text-color; - - span { - color: $secondary-text-color; - } - } - } - - p:last-child { - margin-bottom: 0; - } - } - - &__hero { - margin-bottom: 10px; - - img { - display: block; - margin: 0; - max-width: 100%; - height: auto; - border-radius: 4px; - } - } - - @media screen and (max-width: 840px) { - .information-board { - .container-alt { - padding-right: 20px; - } - - .panel { - position: static; - margin-top: 20px; - width: 100%; - border-radius: 4px; - - .panel-header { - text-align: center; - } - } - } - } - - @media screen and (max-width: 675px) { - .header-wrapper { - padding-top: 0; - - &.compact { - padding-bottom: 0; - } - - &.compact .hero .heading { - text-align: initial; - } } - .header .container-alt, - .features .container-alt { - display: block; - } - } - - .cta { - margin: 20px; - } -} - -.landing { - margin-bottom: 100px; - - @media screen and (max-width: 738px) { - margin-bottom: 0; - } - - &__brand { - display: flex; - justify-content: center; - align-items: center; - padding: 50px; - - .logo { - fill: $primary-text-color; - height: 52px; - } - - @media screen and (max-width: $no-gap-breakpoint) { - padding: 0; - margin-bottom: 30px; - } - } - - .directory { - margin-top: 30px; - background: transparent; - box-shadow: none; - border-radius: 0; - } - - .hero-widget { - margin-top: 30px; - margin-bottom: 0; - - h4 { - padding: 10px; - text-transform: uppercase; - font-weight: 700; - font-size: 13px; - color: $darker-text-color; - } - - &__text { - border-radius: 0; - padding-bottom: 0; - } - - &__footer { - background: $ui-base-color; - padding: 10px; - border-radius: 0 0 4px 4px; - display: flex; - - &__column { - flex: 1 1 50%; - overflow-x: hidden; - } - } - - .account { - padding: 10px 0; - border-bottom: 0; - - .account__display-name { - display: flex; - align-items: center; - } - } - - &__counters__wrapper { - display: flex; - } - - &__counter { - padding: 10px; - width: 50%; - - strong { - font-family: $font-display, sans-serif; - font-size: 15px; - font-weight: 700; - display: block; - } - - span { - font-size: 14px; - color: $darker-text-color; - } - } - } - - .simple_form .user_agreement .label_input > label { - font-weight: 400; - color: $darker-text-color; - } - - .simple_form p.lead { - color: $darker-text-color; - font-size: 15px; - line-height: 20px; - font-weight: 400; - margin-bottom: 25px; - } - - &__grid { - max-width: 960px; - margin: 0 auto; - display: grid; - grid-template-columns: minmax(0, 50%) minmax(0, 50%); - grid-gap: 30px; - - @media screen and (max-width: 738px) { - grid-template-columns: minmax(0, 100%); - grid-gap: 10px; - - &__column-login { - grid-row: 1; - display: flex; - flex-direction: column; - - .box-widget { - order: 2; - flex: 0 0 auto; - } - - .hero-widget { - margin-top: 0; - margin-bottom: 10px; - order: 1; - flex: 0 0 auto; - } - } - - &__column-registration { - grid-row: 2; - } - - .directory { - margin-top: 10px; - } - } - - @media screen and (max-width: $no-gap-breakpoint) { - grid-gap: 0; - - .hero-widget { - display: block; - margin-bottom: 0; - box-shadow: none; - - &__img, - &__img img, - &__footer { - border-radius: 0; - } - } - - .hero-widget, - .box-widget, - .directory__tag { - border-bottom: 1px solid lighten($ui-base-color, 8%); - } - - .directory { - margin-top: 0; - - &__tag { - margin-bottom: 0; - - & > a, - & > div { - border-radius: 0; - box-shadow: none; - } - - &:last-child { - border-bottom: 0; - } - } - } - } - } -} - -.brand { - position: relative; - text-decoration: none; -} - -.brand__tagline { - display: block; - position: absolute; - bottom: -10px; - left: 50px; - width: 300px; - color: $ui-primary-color; - text-decoration: none; - font-size: 14px; - - @media screen and (max-width: $no-gap-breakpoint) { - position: static; - width: auto; - margin-top: 20px; - color: $dark-text-color; - } -} - -.rules-list { - background: darken($ui-base-color, 2%); - border: 1px solid darken($ui-base-color, 8%); - border-radius: 4px; - padding: 0.5em 2.5em !important; - margin-top: 1.85em !important; - - li { - border-bottom: 1px solid lighten($ui-base-color, 4%); - color: $dark-text-color; - padding: 1em; - &:last-child { border-bottom: 0; } } - - &__text { - color: $primary-text-color; - } } diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 77890c467..c2426944b 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -31,23 +31,17 @@ $content-width: 840px; &__toggle { display: none; - background: lighten($ui-base-color, 8%); - height: 48px; + background: darken($ui-base-color, 4%); + border-bottom: 1px solid lighten($ui-base-color, 4%); + align-items: center; &__logo { flex: 1 1 auto; a { - display: inline-block; + display: block; padding: 15px; } - - svg { - fill: $primary-text-color; - height: 20px; - position: relative; - bottom: -2px; - } } &__icon { @@ -55,15 +49,27 @@ $content-width: 840px; color: $darker-text-color; text-decoration: none; flex: 0 0 auto; - font-size: 20px; - padding: 15px; - } + font-size: 18px; + padding: 10px; + margin: 5px 10px; + border-radius: 4px; - a { - &:hover, - &:focus, - &:active { - background: lighten($ui-base-color, 12%); + &:focus { + background: $ui-base-color; + } + + .fa-times { + display: none; + } + + &.active { + .fa-times { + display: block; + } + + .fa-bars { + display: none; + } } } } @@ -79,7 +85,7 @@ $content-width: 840px; display: inherit; margin: inherit; width: inherit; - height: 20px; + height: 25px; } @media screen and (max-width: $no-columns-breakpoint) { @@ -188,24 +194,65 @@ $content-width: 840px; padding-top: 30px; } - &-heading { - display: flex; + &__heading { + margin-bottom: 45px; - padding-bottom: 36px; - border-bottom: 1px solid lighten($ui-base-color, 8%); + &__row { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + margin: -15px -15px 0 0; + + & > * { + margin-top: 15px; + margin-right: 15px; + } + } - margin: -15px -15px 40px 0; + &__tabs { + margin-top: 30px; + width: 100%; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; + & > div { + display: flex; + flex-wrap: wrap; + gap: 5px; + } - & > * { - margin-top: 15px; - margin-right: 15px; + a { + font-size: 14px; + display: inline-flex; + align-items: center; + padding: 7px 10px; + border-radius: 4px; + color: $darker-text-color; + text-decoration: none; + font-weight: 500; + gap: 5px; + white-space: nowrap; + + &:hover, + &:focus, + &:active { + background: lighten($ui-base-color, 4%); + } + + &.selected { + font-weight: 700; + color: $primary-text-color; + background: $ui-highlight-color; + + &:hover, + &:focus, + &:active { + background: lighten($ui-highlight-color, 4%); + } + } + } } - &-actions { + &__actions { display: inline-flex; & > :not(:first-child) { @@ -231,11 +278,7 @@ $content-width: 840px; color: $secondary-text-color; font-size: 24px; line-height: 36px; - font-weight: 400; - - @media screen and (max-width: $no-columns-breakpoint) { - font-weight: 700; - } + font-weight: 700; } h3 { @@ -340,6 +383,14 @@ $content-width: 840px; &.visible { display: block; + position: fixed; + z-index: 10; + width: 100%; + height: calc(100vh - 56px); + left: 0; + bottom: 0; + overflow-y: auto; + background: $ui-base-color; } } @@ -440,6 +491,11 @@ body, } } + & > div { + display: flex; + gap: 5px; + } + strong { font-weight: 500; text-transform: uppercase; @@ -1162,7 +1218,7 @@ a.name-tag, path:first-child { fill: rgba($highlight-text-color, 0.25) !important; - fill-opacity: 1 !important; + fill-opacity: 100% !important; } path:last-child { @@ -1721,3 +1777,67 @@ a.sparkline { } } } + +.history { + counter-reset: step 0; + font-size: 15px; + line-height: 22px; + + li { + counter-increment: step 1; + padding-left: 2.5rem; + padding-bottom: 8px; + position: relative; + margin-bottom: 8px; + + &::before { + position: absolute; + content: counter(step); + font-size: 0.625rem; + font-weight: 500; + left: 0; + display: flex; + justify-content: center; + align-items: center; + width: calc(1.375rem + 1px); + height: calc(1.375rem + 1px); + background: $ui-base-color; + border: 1px solid $highlight-text-color; + color: $highlight-text-color; + border-radius: 8px; + } + + &::after { + position: absolute; + content: ""; + width: 1px; + background: $highlight-text-color; + bottom: 0; + top: calc(1.875rem + 1px); + left: 0.6875rem; + } + + &:last-child { + margin-bottom: 0; + + &::after { + display: none; + } + } + } + + &__entry { + h5 { + font-weight: 500; + color: $primary-text-color; + line-height: 25px; + margin-bottom: 16px; + } + + .status { + border: 1px solid lighten($ui-base-color, 4%); + background: $ui-base-color; + border-radius: 4px; + } + } +} diff --git a/app/javascript/flavours/glitch/styles/compact_header.scss b/app/javascript/flavours/glitch/styles/compact_header.scss deleted file mode 100644 index 4980ab5f1..000000000 --- a/app/javascript/flavours/glitch/styles/compact_header.scss +++ /dev/null @@ -1,34 +0,0 @@ -.compact-header { - h1 { - font-size: 24px; - line-height: 28px; - color: $darker-text-color; - font-weight: 500; - margin-bottom: 20px; - padding: 0 10px; - word-wrap: break-word; - - @media screen and (max-width: 740px) { - text-align: center; - padding: 20px 10px 0; - } - - a { - color: inherit; - text-decoration: none; - } - - small { - font-weight: 400; - color: $secondary-text-color; - } - - img { - display: inline-block; - margin-bottom: -5px; - margin-right: 15px; - width: 36px; - height: 36px; - } - } -} diff --git a/app/javascript/flavours/glitch/styles/components/about.scss b/app/javascript/flavours/glitch/styles/components/about.scss new file mode 100644 index 000000000..ca9ba3ebf --- /dev/null +++ b/app/javascript/flavours/glitch/styles/components/about.scss @@ -0,0 +1,238 @@ +.image { + position: relative; + overflow: hidden; + + &__preview { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + } + + &.loaded &__preview { + display: none; + } + + img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + border: 0; + background: transparent; + opacity: 0; + } + + &.loaded img { + opacity: 1; + } +} + +.about { + padding: 20px; + + @media screen and (min-width: $no-gap-breakpoint) { + border-radius: 4px; + } + + &__header { + margin-bottom: 30px; + + &__hero { + width: 100%; + height: auto; + aspect-ratio: 1.9; + background: lighten($ui-base-color, 4%); + border-radius: 8px; + margin-bottom: 30px; + } + + h1, + p { + text-align: center; + } + + h1 { + font-size: 24px; + line-height: 1.5; + font-weight: 700; + margin-bottom: 10px; + } + + p { + font-size: 16px; + line-height: 24px; + font-weight: 400; + color: $darker-text-color; + } + } + + &__meta { + background: lighten($ui-base-color, 4%); + border-radius: 4px; + display: flex; + margin-bottom: 30px; + font-size: 15px; + + &__column { + box-sizing: border-box; + width: 50%; + padding: 20px; + } + + &__divider { + width: 0; + border: 0; + border-style: solid; + border-color: lighten($ui-base-color, 8%); + border-left-width: 1px; + min-height: calc(100% - 60px); + flex: 0 0 auto; + } + + h4 { + font-size: 15px; + text-transform: uppercase; + color: $darker-text-color; + font-weight: 500; + margin-bottom: 20px; + } + + @media screen and (max-width: 600px) { + display: block; + + h4 { + text-align: center; + } + + &__column { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + } + + &__divider { + min-height: 0; + width: 100%; + border-left-width: 0; + border-top-width: 1px; + } + } + + .layout-multiple-columns & { + display: block; + + h4 { + text-align: center; + } + + &__column { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + } + + &__divider { + min-height: 0; + width: 100%; + border-left-width: 0; + border-top-width: 1px; + } + } + } + + &__mail { + color: $primary-text-color; + text-decoration: none; + font-weight: 500; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + + .getting-started__footer { + padding: 0; + margin-top: 60px; + text-align: center; + font-size: 15px; + line-height: 22px; + + @media screen and (min-width: $no-gap-breakpoint) { + display: none; + } + } + + .account { + padding: 0; + border: 0; + } + + .account__avatar-wrapper { + margin-left: 0; + } + + .account__relationship { + display: none; + } + + &__section { + margin-bottom: 10px; + + &__title { + font-size: 17px; + font-weight: 600; + line-height: 22px; + padding: 20px; + border-radius: 4px; + background: lighten($ui-base-color, 4%); + color: $highlight-text-color; + cursor: pointer; + } + + &.active &__title { + border-radius: 4px 4px 0 0; + } + + &__body { + border: 1px solid lighten($ui-base-color, 4%); + border-top: 0; + padding: 20px; + font-size: 15px; + line-height: 22px; + } + } + + &__domain-blocks { + margin-top: 30px; + width: 100%; + border-collapse: collapse; + break-inside: auto; + + th { + text-align: left; + font-weight: 500; + color: $darker-text-color; + } + + thead tr, + tbody tr { + border-bottom: 1px solid lighten($ui-base-color, 8%); + } + + tbody tr:last-child { + border-bottom: 0; + } + + th, + td { + padding: 8px; + } + } +} diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index 4e912b18b..00519adf1 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -541,6 +541,7 @@ &__buttons { display: flex; align-items: center; + gap: 8px; padding-top: 55px; overflow: hidden; @@ -550,14 +551,6 @@ box-sizing: content-box; padding: 2px; } - - & > .icon-button { - margin-right: 8px; - } - - .button { - margin: 0 8px; - } } &__name { diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index 762e5dbb9..c61815e07 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -42,27 +42,62 @@ &__main { box-sizing: border-box; width: 100%; - max-width: 600px; flex: 0 0 auto; display: flex; flex-direction: column; @media screen and (min-width: $no-gap-breakpoint) { padding: 0 10px; + max-width: 600px; } } } } +$ui-header-height: 55px; + +.ui__header { + display: none; + box-sizing: border-box; + height: $ui-header-height; + position: sticky; + top: 0; + z-index: 2; + justify-content: space-between; + align-items: center; + + &__logo { + display: inline-flex; + padding: 15px; + + .logo { + height: $ui-header-height - 30px; + width: auto; + } + } + + &__links { + display: flex; + align-items: center; + gap: 10px; + padding: 0 10px; + + .button { + flex: 0 0 auto; + } + } +} + .tabs-bar__wrapper { background: darken($ui-base-color, 8%); position: sticky; - top: 0; + top: $ui-header-height; z-index: 2; padding-top: 0; @media screen and (min-width: $no-gap-breakpoint) { padding-top: 10px; + top: 0; } .tabs-bar { @@ -179,6 +214,8 @@ font-size: 16px; padding: 15px; text-decoration: none; + overflow: hidden; + white-space: nowrap; &:hover, &:focus, @@ -322,7 +359,8 @@ > .scrollable { background: $ui-base-color; - border-radius: 0 0 4px 4px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; } } @@ -589,7 +627,6 @@ } .empty-column-indicator, -.error-column, .follow_requests-unlocked_explanation { color: $dark-text-color; background: $ui-base-color; @@ -626,7 +663,48 @@ } .error-column { + padding: 20px; + background: $ui-base-color; + border-radius: 4px; + display: flex; + flex: 1 1 auto; + align-items: center; + justify-content: center; flex-direction: column; + cursor: default; + + &__image { + width: 70%; + max-width: 350px; + margin-top: -50px; + } + + &__message { + text-align: center; + color: $darker-text-color; + font-size: 15px; + line-height: 22px; + + h1 { + font-size: 28px; + line-height: 33px; + font-weight: 700; + margin-bottom: 15px; + color: $primary-text-color; + } + + p { + max-width: 48ch; + } + + &__actions { + margin-top: 30px; + display: flex; + gap: 10px; + align-items: center; + justify-content: center; + } + } } // more fixes for the navbar-under mode @@ -868,7 +946,7 @@ .column-actions { display: flex; - align-items: start; + align-items: flex-start; justify-content: center; padding: 40px; padding-top: 40px; @@ -900,3 +978,28 @@ color: $darker-text-color; } } + +.dismissable-banner { + background: $ui-base-color; + border-bottom: 1px solid lighten($ui-base-color, 8%); + display: flex; + align-items: center; + gap: 30px; + + &__message { + flex: 1 1 auto; + padding: 20px 15px; + cursor: default; + font-size: 14px; + line-height: 18px; + color: $primary-text-color; + } + + &__action { + padding: 15px; + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: center; + } +} diff --git a/app/javascript/flavours/glitch/styles/components/emoji_picker.scss b/app/javascript/flavours/glitch/styles/components/emoji_picker.scss index 0089445e1..bad6949c2 100644 --- a/app/javascript/flavours/glitch/styles/components/emoji_picker.scss +++ b/app/javascript/flavours/glitch/styles/components/emoji_picker.scss @@ -111,7 +111,7 @@ position: relative; input { - font-size: 14px; + font-size: 16px; font-weight: 400; padding: 7px 9px; padding-right: 25px; diff --git a/app/javascript/flavours/glitch/styles/components/explore.scss b/app/javascript/flavours/glitch/styles/components/explore.scss index 05df34eff..bad77fc1c 100644 --- a/app/javascript/flavours/glitch/styles/components/explore.scss +++ b/app/javascript/flavours/glitch/styles/components/explore.scss @@ -3,10 +3,9 @@ } .explore__search-header { - background: $ui-base-color; - display: flex; - align-items: flex-start; + background: darken($ui-base-color, 4%); justify-content: center; + align-items: center; padding: 15px; .search { @@ -15,14 +14,8 @@ } .search__input { - border-radius: 4px; - color: $inverted-text-color; - background: $simple-background-color; + border: 1px solid lighten($ui-base-color, 8%); padding: 10px; - - &::placeholder { - color: $dark-text-color; - } } .search .fa { diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 1c3540b33..28f93018d 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -20,6 +20,11 @@ background: transparent; padding: 0; cursor: pointer; + text-decoration: none; + + &--destructive { + color: $error-value-color; + } &:hover, &:active { @@ -189,6 +194,15 @@ color: $highlight-text-color; } + &.copyable { + transition: background 300ms linear; + } + + &.copied { + background: $valid-value-color; + transition: none; + } + &::-moz-focus-inner { border: 0; } @@ -243,11 +257,12 @@ display: inline-flex; align-items: center; width: auto !important; + padding: 0 4px 0 2px; } &__counter { display: inline-block; - width: 14px; + width: auto; margin-left: 4px; font-size: 12px; font-weight: 500; @@ -1034,6 +1049,7 @@ padding: 10px; padding-top: 20px; z-index: 1; + font-size: 13px; ul { margin-bottom: 10px; @@ -1045,7 +1061,6 @@ p { color: $dark-text-color; - font-size: 13px; margin-bottom: 20px; a { @@ -1778,3 +1793,5 @@ noscript { @import 'announcements'; @import 'explore'; @import 'signed_out'; +@import 'privacy_policy'; +@import 'about'; diff --git a/app/javascript/flavours/glitch/styles/components/privacy_policy.scss b/app/javascript/flavours/glitch/styles/components/privacy_policy.scss new file mode 100644 index 000000000..96cf06742 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/components/privacy_policy.scss @@ -0,0 +1,207 @@ +.privacy-policy { + background: $ui-base-color; + padding: 20px; + + @media screen and (min-width: $no-gap-breakpoint) { + border-radius: 4px; + } + + &__body { + margin-top: 20px; + } +} + +.prose { + color: $secondary-text-color; + font-size: 15px; + line-height: 22px; + + p, + ul, + ol { + margin-top: 1.25em; + margin-bottom: 1.25em; + } + + img { + margin-top: 2em; + margin-bottom: 2em; + } + + video { + margin-top: 2em; + margin-bottom: 2em; + } + + figure { + margin-top: 2em; + margin-bottom: 2em; + + figcaption { + font-size: 0.875em; + line-height: 1.4285714; + margin-top: 0.8571429em; + } + } + + figure > * { + margin-top: 0; + margin-bottom: 0; + } + + h1 { + font-size: 1.5em; + margin-top: 0; + margin-bottom: 1em; + line-height: 1.33; + } + + h2 { + font-size: 1.25em; + margin-top: 1.6em; + margin-bottom: 0.6em; + line-height: 1.6; + } + + h3, + h4, + h5, + h6 { + margin-top: 1.5em; + margin-bottom: 0.5em; + line-height: 1.5; + } + + ol { + counter-reset: list-counter; + } + + li { + margin-top: 0.5em; + margin-bottom: 0.5em; + } + + ol > li { + counter-increment: list-counter; + + &::before { + content: counter(list-counter) "."; + position: absolute; + left: 0; + } + } + + ul > li::before { + content: ""; + position: absolute; + background-color: $darker-text-color; + border-radius: 50%; + width: 0.375em; + height: 0.375em; + top: 0.5em; + left: 0.25em; + } + + ul > li, + ol > li { + position: relative; + padding-left: 1.75em; + } + + & > ul > li p { + margin-top: 0.75em; + margin-bottom: 0.75em; + } + + & > ul > li > *:first-child { + margin-top: 1.25em; + } + + & > ul > li > *:last-child { + margin-bottom: 1.25em; + } + + & > ol > li > *:first-child { + margin-top: 1.25em; + } + + & > ol > li > *:last-child { + margin-bottom: 1.25em; + } + + ul ul, + ul ol, + ol ul, + ol ol { + margin-top: 0.75em; + margin-bottom: 0.75em; + } + + h1, + h2, + h3, + h4, + h5, + h6, + strong, + b { + color: $primary-text-color; + font-weight: 700; + } + + em, + i { + font-style: italic; + } + + a { + color: $highlight-text-color; + text-decoration: underline; + + &:focus, + &:hover, + &:active { + text-decoration: none; + } + } + + code { + font-size: 0.875em; + background: darken($ui-base-color, 8%); + border-radius: 4px; + padding: 0.2em 0.3em; + } + + hr { + border: 0; + border-top: 1px solid lighten($ui-base-color, 4%); + margin-top: 3em; + margin-bottom: 3em; + } + + hr + * { + margin-top: 0; + } + + h2 + * { + margin-top: 0; + } + + h3 + * { + margin-top: 0; + } + + h4 + *, + h5 + *, + h6 + * { + margin-top: 0; + } + + & > :first-child { + margin-top: 0; + } + + & > :last-child { + margin-bottom: 0; + } +} diff --git a/app/javascript/flavours/glitch/styles/components/search.scss b/app/javascript/flavours/glitch/styles/components/search.scss index 17a34db62..70af0f651 100644 --- a/app/javascript/flavours/glitch/styles/components/search.scss +++ b/app/javascript/flavours/glitch/styles/components/search.scss @@ -28,10 +28,6 @@ &:focus { background: lighten($ui-base-color, 4%); } - - @media screen and (max-width: 600px) { - font-size: 16px; - } } .search__icon { @@ -132,6 +128,7 @@ align-items: center; padding: 15px; border-bottom: 1px solid lighten($ui-base-color, 8%); + gap: 15px; &:last-child { border-bottom: 0; @@ -173,16 +170,8 @@ font-size: 24px; font-weight: 500; text-align: right; - padding-right: 15px; - margin-left: 5px; color: $secondary-text-color; text-decoration: none; - - &__asterisk { - color: $darker-text-color; - font-size: 18px; - vertical-align: super; - } } &__sparkline { @@ -191,7 +180,7 @@ path:first-child { fill: rgba($highlight-text-color, 0.25) !important; - fill-opacity: 1 !important; + fill-opacity: 100% !important; } path:last-child { diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss index bbb8b456c..1f1d7d68d 100644 --- a/app/javascript/flavours/glitch/styles/components/single_column.scss +++ b/app/javascript/flavours/glitch/styles/components/single_column.scss @@ -154,97 +154,71 @@ padding-top: 0; } - @media screen and (min-width: 630px) { - .detailed-status { - padding: 15px; + .detailed-status { + padding: 15px; - .media-gallery, - .video-player, - .audio-player { - margin-top: 15px; - } + .media-gallery, + .video-player, + .audio-player { + margin-top: 15px; } + } - .account__header__bar { - padding: 5px 10px; - } + .account__header__bar { + padding: 5px 10px; + } - .navigation-bar, - .compose-form { - padding: 15px; - } + .navigation-bar, + .compose-form { + padding: 15px; + } - .compose-form .compose-form__publish .compose-form__publish-button-wrapper { - padding-top: 15px; - } + .compose-form .compose-form__publish .compose-form__publish-button-wrapper { + padding-top: 15px; + } - .notification__report { - padding: 15px 15px 15px (48px + 15px * 2); - min-height: 48px + 2px; + .notification__report { + padding: 15px 15px 15px (48px + 15px * 2); + min-height: 48px + 2px; - &__avatar { - left: 15px; - top: 17px; - } + &__avatar { + left: 15px; + top: 17px; } + } - .status { - padding: 15px; - min-height: 48px + 2px; + .status { + padding: 15px; + min-height: 48px + 2px; - .media-gallery, - &__action-bar, - .video-player, - .audio-player { - margin-top: 10px; - } + .media-gallery, + &__action-bar, + .video-player, + .audio-player { + margin-top: 10px; } + } - .account { - padding: 15px 10px; + .account { + padding: 15px 10px; - &__header__bio { - margin: 0 -10px; - } + &__header__bio { + margin: 0 -10px; } + } - .notification { - &__message { - padding-top: 15px; - } - - .status { - padding-top: 8px; - } + .notification { + &__message { + padding-top: 15px; + } - .account { - padding-top: 8px; - } + .status { + padding-top: 8px; } - } -} -.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, 2%); - 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: $ui-highlight-color; + .account { + padding-top: 8px; + } } } @@ -261,37 +235,104 @@ .search { margin-bottom: 10px; } -} -@media screen and (max-width: 600px + (285px * 1) + (10px * 1)) { - .columns-area__panels__pane--compositional { + .tabs-bar__link.optional { display: none; } - .with-fab .scrollable .item-list:last-child { - padding-bottom: 5.25rem; + .search-page .search { + display: none; } -} -@media screen and (min-width: 600px + (285px * 1) + (10px * 1)) { - .floating-action-button, - .tabs-bar__link.optional { + .navigation-panel__legal { display: none; } +} - .search-page .search { - display: none; +@media screen and (max-width: $no-gap-breakpoint - 1px) { + $sidebar-width: 285px; + + .columns-area__panels__main { + width: calc(100% - $sidebar-width); + } + + .columns-area__panels { + min-height: calc(100vh - $ui-header-height); + } + + .columns-area__panels__pane--navigational { + min-width: $sidebar-width; + + .columns-area__panels__pane__inner { + width: $sidebar-width; + } + + .navigation-panel { + margin: 0; + background: $ui-base-color; + border-left: 1px solid lighten($ui-base-color, 8%); + height: 100vh; + } + + .navigation-panel__sign-in-banner, + .navigation-panel__logo, + .getting-started__trends { + display: none; + } + + .column-link__icon { + font-size: 18px; + } + } + + .ui__header { + display: flex; + background: $ui-base-color; + border-bottom: 1px solid lighten($ui-base-color, 8%); + } + + .column-header, + .column-back-button, + .scrollable, + .error-column { + border-radius: 0 !important; } } -@media screen and (max-width: 600px + (285px * 2) + (10px * 2)) { +@media screen and (max-width: $no-gap-breakpoint - 285px - 1px) { + $sidebar-width: 55px; + + .columns-area__panels__main { + width: calc(100% - $sidebar-width); + } + .columns-area__panels__pane--navigational { - display: none; + min-width: $sidebar-width; + + .columns-area__panels__pane__inner { + width: $sidebar-width; + } + + .column-link span { + display: none; + } + + .list-panel { + display: none; + } } } -@media screen and (min-width: 600px + (285px * 2) + (10px * 2)) { - .tabs-bar { +.explore__search-header { + display: none; +} + +@media screen and (max-width: $no-gap-breakpoint - 1px) { + .columns-area__panels__pane--compositional { display: none; } + + .explore__search-header { + display: flex; + } } diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index b8d0fdad2..75472646e 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -9,11 +9,7 @@ } .logo-container { - margin: 100px auto 50px; - - @media screen and (max-width: 500px) { - margin: 40px auto 0; - } + margin: 50px auto; h1 { display: flex; @@ -34,7 +30,6 @@ outline: 0; padding: 12px 16px; line-height: 32px; - font-family: $font-display, sans-serif; font-weight: 500; font-size: 14px; } @@ -111,789 +106,3 @@ margin-left: 10px; } } - -.grid-3 { - display: grid; - grid-gap: 10px; - grid-template-columns: 3fr 1fr; - grid-auto-columns: 25%; - grid-auto-rows: max-content; - - .column-0 { - grid-column: 1/3; - grid-row: 1; - } - - .column-1 { - grid-column: 1; - grid-row: 2; - } - - .column-2 { - grid-column: 2; - grid-row: 2; - } - - .column-3 { - grid-column: 1/3; - grid-row: 3; - } - - @media screen and (max-width: $no-gap-breakpoint) { - grid-gap: 0; - grid-template-columns: minmax(0, 100%); - - .column-0 { - grid-column: 1; - } - - .column-1 { - grid-column: 1; - grid-row: 3; - } - - .column-2 { - grid-column: 1; - grid-row: 2; - } - - .column-3 { - grid-column: 1; - grid-row: 4; - } - } -} - -.grid-4 { - display: grid; - grid-gap: 10px; - grid-template-columns: repeat(4, minmax(0, 1fr)); - grid-auto-columns: 25%; - grid-auto-rows: max-content; - - .column-0 { - grid-column: 1 / 5; - grid-row: 1; - } - - .column-1 { - grid-column: 1 / 4; - grid-row: 2; - } - - .column-2 { - grid-column: 4; - grid-row: 2; - } - - .column-3 { - grid-column: 2 / 5; - grid-row: 3; - } - - .column-4 { - grid-column: 1; - grid-row: 3; - } - - .landing-page__call-to-action { - min-height: 100%; - } - - .flash-message { - margin-bottom: 10px; - } - - @media screen and (max-width: 738px) { - grid-template-columns: minmax(0, 50%) minmax(0, 50%); - - .landing-page__call-to-action { - padding: 20px; - display: flex; - align-items: center; - justify-content: center; - } - - .row__information-board { - width: 100%; - justify-content: center; - align-items: center; - } - - .row__mascot { - display: none; - } - } - - @media screen and (max-width: $no-gap-breakpoint) { - grid-gap: 0; - grid-template-columns: minmax(0, 100%); - - .column-0 { - grid-column: 1; - } - - .column-1 { - grid-column: 1; - grid-row: 3; - } - - .column-2 { - grid-column: 1; - grid-row: 2; - } - - .column-3 { - grid-column: 1; - grid-row: 5; - } - - .column-4 { - grid-column: 1; - grid-row: 4; - } - } -} - -.public-layout { - @media screen and (max-width: $no-gap-breakpoint) { - padding-top: 48px; - } - - .container { - max-width: 960px; - - @media screen and (max-width: $no-gap-breakpoint) { - padding: 0; - } - } - - .header { - background: lighten($ui-base-color, 8%); - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - border-radius: 4px; - height: 48px; - margin: 10px 0; - display: flex; - align-items: stretch; - justify-content: center; - flex-wrap: nowrap; - overflow: hidden; - - @media screen and (max-width: $no-gap-breakpoint) { - position: fixed; - width: 100%; - top: 0; - left: 0; - margin: 0; - border-radius: 0; - box-shadow: none; - z-index: 110; - } - - & > div { - flex: 1 1 33.3%; - min-height: 1px; - } - - .nav-left { - display: flex; - align-items: stretch; - justify-content: flex-start; - flex-wrap: nowrap; - } - - .nav-center { - display: flex; - align-items: stretch; - justify-content: center; - flex-wrap: nowrap; - } - - .nav-right { - display: flex; - align-items: stretch; - justify-content: flex-end; - flex-wrap: nowrap; - } - - .brand { - display: block; - padding: 15px; - - .logo { - display: block; - height: 18px; - width: auto; - position: relative; - bottom: -2px; - fill: $primary-text-color; - - @media screen and (max-width: $no-gap-breakpoint) { - height: 20px; - } - } - - &:hover, - &:focus, - &:active { - background: lighten($ui-base-color, 12%); - } - } - - .nav-link { - display: flex; - align-items: center; - padding: 0 1rem; - font-size: 12px; - font-weight: 500; - text-decoration: none; - color: $darker-text-color; - white-space: nowrap; - text-align: center; - - &:hover, - &:focus, - &:active { - text-decoration: underline; - color: $primary-text-color; - } - - @media screen and (max-width: 550px) { - &.optional { - display: none; - } - } - } - - .nav-button { - background: lighten($ui-base-color, 16%); - margin: 8px; - margin-left: 0; - border-radius: 4px; - - &:hover, - &:focus, - &:active { - text-decoration: none; - background: lighten($ui-base-color, 20%); - } - } - } - - $no-columns-breakpoint: 600px; - - .grid { - display: grid; - grid-gap: 10px; - grid-template-columns: minmax(300px, 3fr) minmax(298px, 1fr); - grid-auto-columns: 25%; - grid-auto-rows: max-content; - - .column-0 { - grid-row: 1; - grid-column: 1; - } - - .column-1 { - grid-row: 1; - grid-column: 2; - } - - @media screen and (max-width: $no-columns-breakpoint) { - grid-template-columns: 100%; - grid-gap: 0; - - .column-1 { - display: none; - } - } - } - - .page-header { - @media screen and (max-width: $no-gap-breakpoint) { - border-bottom: 0; - } - } - - .public-account-header { - overflow: hidden; - 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; - } - } - - .logo-button { - padding: 3px 15px; - } - - &__image { - border-radius: 4px 4px 0 0; - overflow: hidden; - height: 300px; - position: relative; - background: darken($ui-base-color, 12%); - - &::after { - content: ""; - display: block; - position: absolute; - width: 100%; - height: 100%; - box-shadow: inset 0 -1px 1px 1px rgba($base-shadow-color, 0.15); - top: 0; - left: 0; - } - - img { - object-fit: cover; - display: block; - width: 100%; - height: 100%; - margin: 0; - border-radius: 4px 4px 0 0; - } - - @media screen and (max-width: 600px) { - height: 200px; - } - } - - &--no-bar { - margin-bottom: 0; - - .public-account-header__image, - .public-account-header__image img { - border-radius: 4px; - - @media screen and (max-width: $no-gap-breakpoint) { - border-radius: 0; - } - } - } - - @media screen and (max-width: $no-gap-breakpoint) { - margin-bottom: 0; - box-shadow: none; - - &__image::after { - display: none; - } - - &__image, - &__image img { - border-radius: 0; - } - } - - &__bar { - position: relative; - margin-top: -80px; - display: flex; - justify-content: flex-start; - - &::before { - content: ""; - display: block; - background: lighten($ui-base-color, 4%); - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 60px; - border-radius: 0 0 4px 4px; - z-index: -1; - } - - .avatar { - display: block; - width: 120px; - height: 120px; - @include avatar-size(120px); - padding-left: 20px - 4px; - flex: 0 0 auto; - - img { - display: block; - width: 100%; - height: 100%; - margin: 0; - border-radius: 50%; - border: 4px solid lighten($ui-base-color, 4%); - background: darken($ui-base-color, 8%); - @include avatar-radius(); - } - } - - @media screen and (max-width: 600px) { - margin-top: 0; - background: lighten($ui-base-color, 4%); - border-radius: 0 0 4px 4px; - padding: 5px; - - &::before { - display: none; - } - - .avatar { - width: 48px; - height: 48px; - @include avatar-size(48px); - padding: 7px 0; - padding-left: 10px; - - img { - border: 0; - border-radius: 4px; - @include avatar-radius(); - } - - @media screen and (max-width: 360px) { - display: none; - } - } - } - - @media screen and (max-width: $no-gap-breakpoint) { - border-radius: 0; - } - - @media screen and (max-width: $no-columns-breakpoint) { - flex-wrap: wrap; - } - } - - &__tabs { - flex: 1 1 auto; - margin-left: 20px; - - &__name { - padding-top: 20px; - padding-bottom: 8px; - - h1 { - font-size: 20px; - line-height: 18px * 1.5; - color: $primary-text-color; - font-weight: 500; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - text-shadow: 1px 1px 1px $base-shadow-color; - - small { - display: block; - font-size: 14px; - color: $primary-text-color; - font-weight: 400; - overflow: hidden; - text-overflow: ellipsis; - } - } - } - - @media screen and (max-width: 600px) { - margin-left: 15px; - display: flex; - justify-content: space-between; - align-items: center; - - &__name { - padding-top: 0; - padding-bottom: 0; - - h1 { - font-size: 16px; - line-height: 24px; - text-shadow: none; - - small { - color: $darker-text-color; - } - } - } - } - - &__tabs { - display: flex; - justify-content: flex-start; - align-items: stretch; - height: 58px; - - .details-counters { - display: flex; - flex-direction: row; - min-width: 300px; - } - - @media screen and (max-width: $no-columns-breakpoint) { - .details-counters { - display: none; - } - } - - .counter { - min-width: 33.3%; - box-sizing: border-box; - flex: 0 0 auto; - color: $darker-text-color; - padding: 10px; - border-right: 1px solid lighten($ui-base-color, 4%); - cursor: default; - text-align: center; - position: relative; - - a { - display: block; - } - - &:last-child { - border-right: 0; - } - - &::after { - display: block; - content: ""; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - border-bottom: 4px solid $ui-primary-color; - opacity: 0.5; - transition: all 400ms ease; - } - - &.active { - &::after { - border-bottom: 4px solid $highlight-text-color; - opacity: 1; - } - - &.inactive::after { - border-bottom-color: $secondary-text-color; - } - } - - &:hover { - &::after { - opacity: 1; - transition-duration: 100ms; - } - } - - a { - text-decoration: none; - color: inherit; - } - - .counter-label { - font-size: 12px; - display: block; - } - - .counter-number { - font-weight: 500; - font-size: 18px; - margin-bottom: 5px; - color: $primary-text-color; - font-family: $font-display, sans-serif; - } - } - - .spacer { - flex: 1 1 auto; - height: 1px; - } - - &__buttons { - padding: 7px 8px; - } - } - } - - &__extra { - display: none; - margin-top: 4px; - - .public-account-bio { - border-radius: 0; - box-shadow: none; - background: transparent; - margin: 0 -5px; - - .account__header__fields { - border-top: 1px solid lighten($ui-base-color, 12%); - } - - .roles { - display: none; - } - } - - &__links { - margin-top: -15px; - font-size: 14px; - color: $darker-text-color; - - a { - display: inline-block; - color: $darker-text-color; - text-decoration: none; - padding: 15px; - font-weight: 500; - - strong { - font-weight: 700; - color: $primary-text-color; - } - } - } - - @media screen and (max-width: $no-columns-breakpoint) { - display: block; - flex: 100%; - } - } - } - - .account__section-headline { - border-radius: 4px 4px 0 0; - - @media screen and (max-width: $no-gap-breakpoint) { - border-radius: 0; - } - } - - .detailed-status__meta { - margin-top: 25px; - } - - .public-account-bio { - background: lighten($ui-base-color, 8%); - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - border-radius: 4px; - overflow: hidden; - margin-bottom: 10px; - - @media screen and (max-width: $no-gap-breakpoint) { - box-shadow: none; - margin-bottom: 0; - border-radius: 0; - } - - .account__header__fields { - margin: 0; - border-top: 0; - - a { - color: $highlight-text-color; - } - - dl:first-child .verified { - border-radius: 0 4px 0 0; - } - - .verified a { - color: $valid-value-color; - } - } - - .account__header__content { - padding: 20px; - padding-bottom: 0; - color: $primary-text-color; - } - - &__extra, - .roles { - padding: 20px; - font-size: 14px; - color: $darker-text-color; - } - - .roles { - padding-bottom: 0; - } - } - - .directory__list { - display: grid; - grid-gap: 10px; - grid-template-columns: minmax(0, 50%) minmax(0, 50%); - - .account-card { - display: flex; - flex-direction: column; - } - - @media screen and (max-width: $no-gap-breakpoint) { - display: block; - - .account-card { - margin-bottom: 10px; - display: block; - } - } - } - - .card-grid { - display: flex; - flex-wrap: wrap; - min-width: 100%; - margin: 0 -5px; - - & > div { - box-sizing: border-box; - flex: 1 0 auto; - width: 300px; - padding: 0 5px; - margin-bottom: 10px; - max-width: 33.333%; - - @media screen and (max-width: 900px) { - max-width: 50%; - } - - @media screen and (max-width: 600px) { - max-width: 100%; - } - } - - @media screen and (max-width: $no-gap-breakpoint) { - margin: 0; - border-top: 1px solid lighten($ui-base-color, 8%); - - & > div { - width: 100%; - padding: 0; - margin-bottom: 0; - border-bottom: 1px solid lighten($ui-base-color, 8%); - - &:last-child { - border-bottom: 0; - } - - .card__bar { - background: $ui-base-color; - - &:hover, - &:active, - &:focus { - background: lighten($ui-base-color, 4%); - } - } - } - } - } -} diff --git a/app/javascript/flavours/glitch/styles/contrast/diff.scss b/app/javascript/flavours/glitch/styles/contrast/diff.scss index 9bd31cd7e..a9a203960 100644 --- a/app/javascript/flavours/glitch/styles/contrast/diff.scss +++ b/app/javascript/flavours/glitch/styles/contrast/diff.scss @@ -1,9 +1,5 @@ // components.scss -.rich-formatting a, -.rich-formatting p a, -.rich-formatting li a, -.landing-page__short-description p a, .status__content a, .reply-indicator__content a { color: lighten($ui-highlight-color, 12%); diff --git a/app/javascript/flavours/glitch/styles/dashboard.scss b/app/javascript/flavours/glitch/styles/dashboard.scss index 9b06b44d6..bb103e9ce 100644 --- a/app/javascript/flavours/glitch/styles/dashboard.scss +++ b/app/javascript/flavours/glitch/styles/dashboard.scss @@ -39,7 +39,6 @@ font-size: 24px; line-height: 21px; color: $primary-text-color; - font-family: $font-display, sans-serif; margin-bottom: 20px; line-height: 30px; } diff --git a/app/javascript/flavours/glitch/styles/footer.scss b/app/javascript/flavours/glitch/styles/footer.scss deleted file mode 100644 index 0c3e42033..000000000 --- a/app/javascript/flavours/glitch/styles/footer.scss +++ /dev/null @@ -1,152 +0,0 @@ -.public-layout { - .footer { - text-align: left; - padding-top: 20px; - padding-bottom: 60px; - font-size: 12px; - color: lighten($ui-base-color, 34%); - - @media screen and (max-width: $no-gap-breakpoint) { - padding-left: 20px; - padding-right: 20px; - } - - .grid { - display: grid; - grid-gap: 10px; - grid-template-columns: 1fr 1fr 2fr 1fr 1fr; - - .column-0 { - grid-column: 1; - grid-row: 1; - min-width: 0; - } - - .column-1 { - grid-column: 2; - grid-row: 1; - min-width: 0; - } - - .column-2 { - grid-column: 3; - grid-row: 1; - min-width: 0; - text-align: center; - - h4 a { - color: lighten($ui-base-color, 34%); - } - } - - .column-3 { - grid-column: 4; - grid-row: 1; - min-width: 0; - } - - .column-4 { - grid-column: 5; - grid-row: 1; - min-width: 0; - } - - @media screen and (max-width: 690px) { - grid-template-columns: 1fr 2fr 1fr; - - .column-0, - .column-1 { - grid-column: 1; - } - - .column-1 { - grid-row: 2; - } - - .column-2 { - grid-column: 2; - } - - .column-3, - .column-4 { - grid-column: 3; - } - - .column-4 { - grid-row: 2; - } - } - - @media screen and (max-width: 600px) { - .column-1 { - display: block; - } - } - - @media screen and (max-width: $no-gap-breakpoint) { - .column-0, - .column-1, - .column-3, - .column-4 { - display: none; - } - - .column-2 h4 { - display: none; - } - } - } - - .legal-xs { - display: none; - text-align: center; - padding-top: 20px; - - @media screen and (max-width: $no-gap-breakpoint) { - display: block; - } - } - - h4 { - text-transform: uppercase; - font-weight: 700; - margin-bottom: 8px; - color: $darker-text-color; - - a { - color: inherit; - text-decoration: none; - } - } - - ul a, - .legal-xs a { - text-decoration: none; - color: lighten($ui-base-color, 34%); - - &:hover, - &:active, - &:focus { - text-decoration: underline; - } - } - - .brand { - .logo { - display: block; - height: 36px; - width: auto; - margin: 0 auto; - color: lighten($ui-base-color, 34%); - } - - &:hover, - &:focus, - &:active { - .logo { - color: lighten($ui-base-color, 38%); - } - } - } - } -} diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index 8ae2b5bd8..ef0ca1fcd 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -6,9 +6,10 @@ code { } .form-container { - max-width: 400px; + max-width: 450px; padding: 20px; - margin: 0 auto; + padding-bottom: 50px; + margin: 50px auto; } .indicator-icon { @@ -123,10 +124,22 @@ code { } .title { - color: #d9e1e8; - font-size: 20px; - line-height: 28px; - font-weight: 400; + font-size: 28px; + line-height: 33px; + font-weight: 700; + margin-bottom: 15px; + } + + .lead { + font-size: 17px; + line-height: 22px; + color: $secondary-text-color; + margin-bottom: 30px; + } + + .rules-list { + font-size: 17px; + line-height: 22px; margin-bottom: 30px; } @@ -231,7 +244,7 @@ code { & > label { font-family: inherit; - font-size: 16px; + font-size: 14px; color: $primary-text-color; display: block; font-weight: 500; @@ -268,6 +281,20 @@ code { .input:last-child { margin-bottom: 0; } + + &__thumbnail { + display: block; + margin: 0; + margin-bottom: 10px; + max-width: 100%; + height: auto; + border-radius: 4px; + background: url("images/void.png"); + + &:last-child { + margin-bottom: 0; + } + } } .fields-row { @@ -368,6 +395,7 @@ code { input[type=email], input[type=password], input[type=url], + input[type=datetime-local], textarea { box-sizing: border-box; font-size: 16px; @@ -408,7 +436,8 @@ code { input[type=text], input[type=number], input[type=email], - input[type=password] { + input[type=password], + input[type=datetime-local] { &:focus:invalid:not(:placeholder-shown), &:required:invalid:not(:placeholder-shown) { border-color: lighten($error-red, 12%); @@ -424,6 +453,7 @@ code { input[type=number], input[type=email], input[type=password], + input[type=datetime-local], textarea, select { border-color: lighten($error-red, 12%); @@ -451,6 +481,11 @@ code { } } + .stacked-actions { + margin-top: 30px; + margin-bottom: 15px; + } + button, .button, .block-button { @@ -502,6 +537,16 @@ code { } } + .button.button-tertiary { + padding: 9px; + + &:hover, + &:focus, + &:active { + padding: 10px; + } + } + select { appearance: none; box-sizing: border-box; @@ -556,41 +601,6 @@ code { } } } - - &__overlay-area { - position: relative; - - &__blurred form { - filter: blur(2px); - } - - &__overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - background: rgba($ui-base-color, 0.65); - border-radius: 4px; - margin-left: -4px; - margin-top: -4px; - padding: 4px; - - &__content { - text-align: center; - - &.rich-formatting { - &, - p { - color: $primary-text-color; - } - } - } - } - } } .block-icon { @@ -861,24 +871,7 @@ code { } } -.table-form { - p { - margin-bottom: 15px; - - strong { - font-weight: 500; - - @each $lang in $cjk-langs { - &:lang(#{$lang}) { - font-weight: 700; - } - } - } - } -} - -.simple_form, -.table-form { +.simple_form { .warning { box-sizing: border-box; background: rgba($error-value-color, 0.5); diff --git a/app/javascript/flavours/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss index f808773f3..fbb02c788 100644 --- a/app/javascript/flavours/glitch/styles/index.scss +++ b/app/javascript/flavours/glitch/styles/index.scss @@ -2,7 +2,6 @@ @import 'variables'; @import 'styles/fonts/roboto'; @import 'styles/fonts/roboto-mono'; -@import 'styles/fonts/montserrat'; @import 'reset'; @import 'basics'; @@ -10,8 +9,6 @@ @import 'containers'; @import 'lists'; @import 'modal'; -@import 'footer'; -@import 'compact_header'; @import 'widgets'; @import 'forms'; @import 'accounts'; diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss index d16f23aed..d4ac55847 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss @@ -354,45 +354,11 @@ } } -.public-layout { - .header, - .public-account-header, - .public-account-bio { - box-shadow: none; - } - - .header { - background: lighten($ui-base-color, 12%); - } - - .public-account-header { - &__image { - background: lighten($ui-base-color, 12%); - - &::after { - box-shadow: none; - } - } - - &__tabs { - &__name { - h1, - h1 small { - color: $white; - } - } - } - } -} - .account__section-headline a.active::after { border-color: transparent transparent $white; } .hero-widget, -.box-widget, -.contact-widget, -.landing-page__information.contact-widget, .moved-account-widget, .memoriam-widget, .activity-stream, diff --git a/app/javascript/flavours/glitch/styles/rtl.scss b/app/javascript/flavours/glitch/styles/rtl.scss index d0153c9f9..31d1de376 100644 --- a/app/javascript/flavours/glitch/styles/rtl.scss +++ b/app/javascript/flavours/glitch/styles/rtl.scss @@ -30,16 +30,6 @@ body.rtl { right: -26px; } - .landing-page__logo { - margin-right: 0; - margin-left: 20px; - } - - .landing-page .features-list .features-list__row .visual { - margin-left: 0; - margin-right: 15px; - } - .column-link__icon, .column-header__icon { margin-right: 0; @@ -327,44 +317,6 @@ body.rtl { margin-left: 45px; } - .landing-page .header-wrapper .mascot { - right: 60px; - left: auto; - } - - .landing-page__call-to-action .row__information-board { - direction: rtl; - } - - .landing-page .header .hero .floats .float-1 { - left: -120px; - right: auto; - } - - .landing-page .header .hero .floats .float-2 { - left: 210px; - right: auto; - } - - .landing-page .header .hero .floats .float-3 { - left: 110px; - right: auto; - } - - .landing-page .header .links .brand img { - left: 0; - } - - .landing-page .fa-external-link { - padding-right: 5px; - padding-left: 0 !important; - } - - .landing-page .features #mastodon-timeline { - margin-right: 0; - margin-left: 30px; - } - @media screen and (min-width: 631px) { .column, .drawer { @@ -392,32 +344,6 @@ body.rtl { padding-right: 0; } - .public-layout { - .header { - .nav-button { - margin-left: 8px; - margin-right: 0; - } - } - - .public-account-header__tabs { - margin-left: 0; - margin-right: 20px; - } - } - - .landing-page__information { - .account__display-name { - margin-right: 0; - margin-left: 5px; - } - - .account__avatar-wrapper { - margin-left: 12px; - margin-right: 0; - } - } - .card__bar .display-name { margin-left: 0; margin-right: 15px; diff --git a/app/javascript/flavours/glitch/styles/statuses.scss b/app/javascript/flavours/glitch/styles/statuses.scss index c302fc0d0..947a5d3ae 100644 --- a/app/javascript/flavours/glitch/styles/statuses.scss +++ b/app/javascript/flavours/glitch/styles/statuses.scss @@ -133,8 +133,7 @@ a.button.logo-button { justify-content: center; } -.embed, -.public-layout { +.embed { .status__content[data-spoiler=folded] { .e-content { display: none; @@ -204,8 +203,7 @@ a.button.logo-button { } // Styling from upstream's WebUI, as public pages use the same layout -.embed, -.public-layout { +.embed { .status { .status__info { font-size: 15px; @@ -244,8 +242,7 @@ a.button.logo-button { } .rtl { - .embed, - .public-layout { + .embed { .status { padding-left: 10px; padding-right: 68px; diff --git a/app/javascript/flavours/glitch/styles/variables.scss b/app/javascript/flavours/glitch/styles/variables.scss index 65758e6e0..b865b5a2d 100644 --- a/app/javascript/flavours/glitch/styles/variables.scss +++ b/app/javascript/flavours/glitch/styles/variables.scss @@ -51,7 +51,7 @@ $media-modal-media-max-width: 100%; // put margins on top and bottom of image to avoid the screen covered by image. $media-modal-media-max-height: 80%; -$no-gap-breakpoint: 415px; +$no-gap-breakpoint: 1175px; $font-sans-serif: 'mastodon-font-sans-serif' !default; $font-display: 'mastodon-font-display' !default; diff --git a/app/javascript/flavours/glitch/styles/widgets.scss b/app/javascript/flavours/glitch/styles/widgets.scss index a88f3b2c7..fd091ee89 100644 --- a/app/javascript/flavours/glitch/styles/widgets.scss +++ b/app/javascript/flavours/glitch/styles/widgets.scss @@ -108,13 +108,6 @@ } } -.box-widget { - padding: 20px; - border-radius: 4px; - background: $ui-base-color; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); -} - .placeholder-widget { padding: 16px; border-radius: 4px; @@ -124,47 +117,6 @@ margin-bottom: 10px; } -.contact-widget { - min-height: 100%; - font-size: 15px; - color: $darker-text-color; - line-height: 20px; - word-wrap: break-word; - font-weight: 400; - padding: 0; - - h4 { - padding: 10px; - text-transform: uppercase; - font-weight: 700; - font-size: 13px; - color: $darker-text-color; - } - - .account { - border-bottom: 0; - padding: 10px 0; - padding-top: 5px; - } - - & > a { - display: inline-block; - padding: 10px; - padding-top: 0; - color: $darker-text-color; - text-decoration: none; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - &:hover, - &:focus, - &:active { - text-decoration: underline; - } - } -} - .moved-account-widget { padding: 15px; padding-bottom: 20px; @@ -245,37 +197,6 @@ margin-bottom: 10px; } -.page-header { - background: lighten($ui-base-color, 8%); - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - border-radius: 4px; - padding: 60px 15px; - text-align: center; - margin: 10px 0; - - h1 { - color: $primary-text-color; - font-size: 36px; - line-height: 1.1; - font-weight: 700; - margin-bottom: 10px; - } - - p { - font-size: 15px; - color: $darker-text-color; - } - - @media screen and (max-width: $no-gap-breakpoint) { - margin-top: 0; - background: lighten($ui-base-color, 4%); - - h1 { - font-size: 24px; - } - } -} - .directory { background: $ui-base-color; border-radius: 4px; @@ -357,34 +278,6 @@ } } -.avatar-stack { - display: flex; - justify-content: flex-end; - - .account__avatar { - flex: 0 0 auto; - width: 36px; - height: 36px; - border-radius: 50%; - position: relative; - margin-left: -10px; - background: darken($ui-base-color, 8%); - border: 2px solid $ui-base-color; - - &:nth-child(1) { - z-index: 1; - } - - &:nth-child(2) { - z-index: 2; - } - - &:nth-child(3) { - z-index: 3; - } - } -} - .accounts-table { width: 100%; @@ -486,11 +379,7 @@ .moved-account-widget, .memoriam-widget, -.box-widget, -.contact-widget, -.landing-page__information.contact-widget, -.directory, -.page-header { +.directory { @media screen and (max-width: $no-gap-breakpoint) { margin-bottom: 0; box-shadow: none; @@ -498,88 +387,6 @@ } } -$maximum-width: 1235px; -$fluid-breakpoint: $maximum-width + 20px; - -.statuses-grid { - min-height: 600px; - - @media screen and (max-width: 640px) { - width: 100% !important; // Masonry layout is unnecessary at this width - } - - &__item { - width: math.div(960px - 20px, 3); - - @media screen and (max-width: $fluid-breakpoint) { - width: math.div(940px - 20px, 3); - } - - @media screen and (max-width: 640px) { - width: 100%; - } - - @media screen and (max-width: $no-gap-breakpoint) { - width: 100vw; - } - } - - .detailed-status { - border-radius: 4px; - - @media screen and (max-width: $no-gap-breakpoint) { - border-top: 1px solid lighten($ui-base-color, 16%); - } - - &.compact { - .detailed-status__meta { - margin-top: 15px; - } - - .status__content { - font-size: 15px; - line-height: 20px; - - .emojione { - width: 20px; - height: 20px; - margin: -3px 0 0; - } - - .status__content__spoiler-link { - line-height: 20px; - margin: 0; - } - } - - .media-gallery, - .status-card, - .video-player { - margin-top: 15px; - } - } - } -} - -.notice-widget { - margin-bottom: 10px; - color: $darker-text-color; - - p { - margin-bottom: 10px; - - &:last-child { - margin-bottom: 0; - } - } - - a { - font-size: 14px; - line-height: 20px; - } -} - -.notice-widget, .placeholder-widget { a { text-decoration: none; @@ -593,37 +400,3 @@ $fluid-breakpoint: $maximum-width + 20px; } } } - -.table-of-contents { - background: darken($ui-base-color, 4%); - min-height: 100%; - font-size: 14px; - border-radius: 4px; - - li a { - display: block; - font-weight: 500; - padding: 15px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-decoration: none; - color: $primary-text-color; - border-bottom: 1px solid lighten($ui-base-color, 4%); - - &:hover, - &:focus, - &:active { - text-decoration: underline; - } - } - - li:last-child a { - border-bottom: 0; - } - - li ul { - padding-left: 20px; - border-bottom: 1px solid lighten($ui-base-color, 4%); - } -} diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml index f3c7fac7e..e85dd74e1 100644 --- a/app/javascript/flavours/glitch/theme.yml +++ b/app/javascript/flavours/glitch/theme.yml @@ -1,6 +1,5 @@ # (REQUIRED) The location of the pack files. pack: - about: packs/about.js admin: - packs/admin.js - packs/public.js diff --git a/app/javascript/flavours/glitch/utils/backend_links.js b/app/javascript/flavours/glitch/utils/backend_links.js index d0ae63419..2028a1e60 100644 --- a/app/javascript/flavours/glitch/utils/backend_links.js +++ b/app/javascript/flavours/glitch/utils/backend_links.js @@ -3,7 +3,7 @@ export const profileLink = '/settings/profile'; export const signOutLink = '/auth/sign_out'; export const privacyPolicyLink = '/privacy-policy'; export const accountAdminLink = (id) => `/admin/accounts/${id}`; -export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses?id=${status_id}`; +export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses/${status_id}`; export const filterEditLink = (id) => `/filters/${id}/edit`; export const relationshipsLink = '/relationships'; export const securityLink = '/auth/edit'; diff --git a/app/javascript/flavours/vanilla/theme.yml b/app/javascript/flavours/vanilla/theme.yml index 9173d4ec9..5cb76b721 100644 --- a/app/javascript/flavours/vanilla/theme.yml +++ b/app/javascript/flavours/vanilla/theme.yml @@ -1,6 +1,5 @@ # (REQUIRED) The location of the pack files inside `pack_directory`. pack: - about: about.js admin: - admin.js - public.js |