From 101f8616feb845f70ef89fa0d0b3ebc37c472930 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 5 Jul 2021 19:16:06 +0200 Subject: [Glitch] Fix pop-in player display when poster has long username or handle Port 1381e0e1d9f27bd108d8b9349896f10ffe996cb2 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/components/status.scss | 1 + 1 file changed, 1 insertion(+) (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index e906a7261..e9d30544f 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -1095,6 +1095,7 @@ a.status-card.compact:hover { &__account { display: flex; text-decoration: none; + overflow: hidden; } .account__avatar { -- cgit From a85eb7d930c0b1acfef9a25c102d902f63f3d379 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 12 Jul 2021 08:07:04 +0200 Subject: Fix follow suggestions scrolling on mobile view Also simplify the CSS a bit and bring it closer to upstream. --- .../features/compose/components/search_results.js | 16 ++--- .../flavours/glitch/styles/components/drawer.scss | 76 ++++++++-------------- .../flavours/glitch/styles/components/search.scss | 11 +++- 3 files changed, 41 insertions(+), 62 deletions(-) (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/flavours/glitch/features/compose/components/search_results.js b/app/javascript/flavours/glitch/features/compose/components/search_results.js index a0f86a06a..9a76e5418 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search_results.js +++ b/app/javascript/flavours/glitch/features/compose/components/search_results.js @@ -71,7 +71,7 @@ class SearchResults extends ImmutablePureComponent { ); } else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) { statuses = ( -
+
@@ -87,7 +87,7 @@ class SearchResults extends ImmutablePureComponent { if (results.get('accounts') && results.get('accounts').size > 0) { count += results.get('accounts').size; accounts = ( -
+
{results.get('accounts').map(accountId => )} @@ -100,7 +100,7 @@ class SearchResults extends ImmutablePureComponent { if (results.get('statuses') && results.get('statuses').size > 0) { count += results.get('statuses').size; statuses = ( -
+
{results.get('statuses').map(statusId => )} @@ -113,7 +113,7 @@ class SearchResults extends ImmutablePureComponent { if (results.get('hashtags') && results.get('hashtags').size > 0) { count += results.get('hashtags').size; hashtags = ( -
+
{results.get('hashtags').map(hashtag => )} @@ -131,11 +131,9 @@ class SearchResults extends ImmutablePureComponent { -
- {accounts} - {statuses} - {hashtags} -
+ {accounts} + {statuses} + {hashtags}
); }; diff --git a/app/javascript/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss index b6d06f53a..61969abee 100644 --- a/app/javascript/flavours/glitch/styles/components/drawer.scss +++ b/app/javascript/flavours/glitch/styles/components/drawer.scss @@ -120,20 +120,22 @@ } .drawer--results { - background: $ui-base-color; - overflow: hidden; - display: flex; - flex-direction: column; - flex: 1 1 auto; + overflow-x: hidden; + overflow-y: scroll; +} - & > header { - color: $dark-text-color; - background: lighten($ui-base-color, 2%); +.search-results__section { + margin-bottom: 5px; + + h5 { + background: darken($ui-base-color, 4%); + border-bottom: 1px solid lighten($ui-base-color, 8%); + cursor: default; + display: flex; padding: 15px; font-weight: 500; font-size: 16px; - cursor: default; - flex: 0 0 auto; + color: $dark-text-color; .fa { display: inline-block; @@ -141,48 +143,22 @@ } } - & > .search-results__contents { - overflow-x: hidden; - overflow-y: scroll; - flex: 1 1 auto; - - & > section { - margin-bottom: 5px; - - h5 { - background: darken($ui-base-color, 4%); - border-bottom: 1px solid lighten($ui-base-color, 8%); - cursor: default; - display: flex; - padding: 15px; - font-weight: 500; - font-size: 16px; - color: $dark-text-color; - - .fa { - display: inline-block; - margin-right: 5px; - } - } + .account:last-child, + & > div:last-child .status { + border-bottom: 0; + } - .account:last-child, - & > div:last-child .status { - border-bottom: 0; - } + & > .hashtag { + display: block; + padding: 10px; + color: $secondary-text-color; + text-decoration: none; - & > .hashtag { - display: block; - padding: 10px; - color: $secondary-text-color; - text-decoration: none; - - &:hover, - &:active, - &:focus { - color: lighten($secondary-text-color, 4%); - text-decoration: underline; - } - } + &:hover, + &:active, + &:focus { + color: lighten($secondary-text-color, 4%); + text-decoration: underline; } } } diff --git a/app/javascript/flavours/glitch/styles/components/search.scss b/app/javascript/flavours/glitch/styles/components/search.scss index eec2e64d6..929769130 100644 --- a/app/javascript/flavours/glitch/styles/components/search.scss +++ b/app/javascript/flavours/glitch/styles/components/search.scss @@ -94,10 +94,15 @@ .search-results__header { color: $dark-text-color; background: lighten($ui-base-color, 2%); - border-bottom: 1px solid darken($ui-base-color, 4%); - padding: 15px 10px; - font-size: 14px; + padding: 15px; font-weight: 500; + font-size: 16px; + cursor: default; + + .fa { + display: inline-block; + margin-right: 5px; + } } .search-results__info { -- cgit From 8d55cb7d711723fe2a35377ee2443cb11de11cc3 Mon Sep 17 00:00:00 2001 From: Jeong Arm Date: Thu, 5 Aug 2021 20:05:32 +0900 Subject: [Glitch] Fix trends layout Port 6e0ab6814f4d3906c035e10a9cedbc41ae5967e9 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/components/index.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 24f750e1d..04bd8805a 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -977,13 +977,13 @@ } @media screen and (max-height: 810px) { - .trends__item:nth-child(3) { + .trends__item:nth-of-type(3) { display: none; } } @media screen and (max-height: 720px) { - .trends__item:nth-child(2) { + .trends__item:nth-of-type(2) { display: none; } } -- cgit From 0e62c38b029c834363580868f7d5d486e565ad93 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 11 Aug 2021 17:48:55 +0200 Subject: [Glitch] Fix download button color in audio player Port aaf24d3093d565461b0051d2238d8b74db63a041 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/components/media.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss index 855cd07a9..8a551be73 100644 --- a/app/javascript/flavours/glitch/styles/components/media.scss +++ b/app/javascript/flavours/glitch/styles/components/media.scss @@ -400,7 +400,8 @@ opacity: 0.2; } - .video-player__buttons button { + .video-player__buttons button, + .video-player__buttons a { color: currentColor; opacity: 0.75; -- cgit From cef109e04661b1627acf9c98f6d00ea0629d41f4 Mon Sep 17 00:00:00 2001 From: Mélanie Chauvel Date: Fri, 1 Oct 2021 00:55:51 +0200 Subject: [Glitch] Improve hover and focus style in columns settings Port 900481b7fa638119b826ed888fc8eaca962ecf55 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/components/column_header.js | 6 +++--- .../flavours/glitch/styles/components/columns.scss | 16 +++++++++++++--- .../flavours/glitch/styles/components/index.scss | 4 ++-- app/javascript/flavours/glitch/styles/rtl.scss | 19 ++++++++++++++----- 4 files changed, 32 insertions(+), 13 deletions(-) (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/flavours/glitch/components/column_header.js b/app/javascript/flavours/glitch/components/column_header.js index ccd0714f1..500612093 100644 --- a/app/javascript/flavours/glitch/components/column_header.js +++ b/app/javascript/flavours/glitch/components/column_header.js @@ -124,8 +124,8 @@ class ColumnHeader extends React.PureComponent { moveButtons = (
- - + +
); } else if (multiColumn && this.props.onPin) { @@ -146,8 +146,8 @@ class ColumnHeader extends React.PureComponent { ]; if (multiColumn) { - collapsedContent.push(moveButtons); collapsedContent.push(pinButton); + collapsedContent.push(moveButtons); } if (children || (multiColumn && this.props.onPin)) { diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index ad17ed4b0..04d9b4168 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -437,12 +437,17 @@ } .column-header__setting-btn { - &:hover { + &:hover, + &:focus { color: $darker-text-color; text-decoration: underline; } } +.column-header__collapsible__extra + .column-header__setting-btn { + padding-top: 5px; +} + .column-header__permission-btn { display: inline; font-weight: inherit; @@ -453,10 +458,15 @@ float: right; .column-header__setting-btn { - padding: 0 10px; + padding: 5px; + + &:first-child { + padding-right: 7px; + } &:last-child { - padding-right: 0; + padding-left: 7px; + margin-left: 5px; } } } diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 04bd8805a..2281a4bb3 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -829,7 +829,7 @@ transition: background-color 0.2s ease; } -.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { +.react-toggle:is(:hover, :focus-within):not(.react-toggle--disabled) .react-toggle-track { background-color: darken($ui-base-color, 10%); } @@ -837,7 +837,7 @@ background-color: $ui-highlight-color; } -.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track { +.react-toggle--checked:is(:hover, :focus-within):not(.react-toggle--disabled) .react-toggle-track { background-color: lighten($ui-highlight-color, 10%); } diff --git a/app/javascript/flavours/glitch/styles/rtl.scss b/app/javascript/flavours/glitch/styles/rtl.scss index f6a90d271..61e95ad24 100644 --- a/app/javascript/flavours/glitch/styles/rtl.scss +++ b/app/javascript/flavours/glitch/styles/rtl.scss @@ -112,6 +112,20 @@ body.rtl { .column-header__setting-arrows { float: left; + + .column-header__setting-btn { + &:first-child { + padding-left: 7px; + padding-right: 5px; + } + + &:last-child { + padding-right: 7px; + padding-left: 5px; + margin-right: 5px; + margin-left: 0; + } + } } .setting-toggle__label { @@ -428,11 +442,6 @@ body.rtl { margin-left: 5px; } - .column-header__setting-arrows .column-header__setting-btn:last-child { - padding-left: 0; - padding-right: 10px; - } - .simple_form .input.radio_buttons .radio > label input { left: auto; right: 0; -- cgit From 7aec1bc30862b81de8dcb43b61f8fdd13c935ecd Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 14 Oct 2021 20:44:59 +0200 Subject: [Glitch] Add graphs and retention metrics to admin dashboard (#16829) Port 07341e7aa60fe7c7d4f298136af99276820940e7 to glitch-soc Signed-off-by: Claire --- app/javascript/core/admin.js | 20 --- .../flavours/glitch/components/admin/Counter.js | 115 ++++++++++++ .../flavours/glitch/components/admin/Dimension.js | 92 ++++++++++ .../flavours/glitch/components/admin/Retention.js | 141 +++++++++++++++ .../flavours/glitch/components/admin/Trends.js | 73 ++++++++ .../flavours/glitch/components/hashtag.js | 61 +++---- .../flavours/glitch/components/skeleton.js | 11 ++ .../flavours/glitch/containers/admin_component.js | 26 +++ .../flavours/glitch/containers/media_container.js | 2 +- .../features/compose/components/search_results.js | 2 +- .../features/getting_started/components/trends.js | 2 +- app/javascript/flavours/glitch/packs/admin.js | 24 +++ app/javascript/flavours/glitch/styles/admin.scss | 193 ++++++++++++++++++++- .../flavours/glitch/styles/components/search.scss | 53 +++++- .../flavours/glitch/styles/dashboard.scss | 57 ++++-- app/javascript/flavours/glitch/theme.yml | 2 +- app/javascript/flavours/glitch/util/numbers.js | 8 + app/javascript/flavours/vanilla/theme.yml | 2 +- app/javascript/packs/admin.js | 24 +++ 19 files changed, 838 insertions(+), 70 deletions(-) create mode 100644 app/javascript/flavours/glitch/components/admin/Counter.js create mode 100644 app/javascript/flavours/glitch/components/admin/Dimension.js create mode 100644 app/javascript/flavours/glitch/components/admin/Retention.js create mode 100644 app/javascript/flavours/glitch/components/admin/Trends.js create mode 100644 app/javascript/flavours/glitch/components/skeleton.js create mode 100644 app/javascript/flavours/glitch/containers/admin_component.js create mode 100644 app/javascript/flavours/glitch/packs/admin.js create mode 100644 app/javascript/packs/admin.js (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index 8817c09b6..d2db89ca7 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -101,24 +101,4 @@ ready(() => { const registrationMode = document.getElementById('form_admin_settings_registrations_mode'); if (registrationMode) onChangeRegistrationMode(registrationMode); - - const React = require('react'); - const ReactDOM = require('react-dom'); - - [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => { - const componentName = element.getAttribute('data-admin-component'); - const { locale, ...componentProps } = JSON.parse(element.getAttribute('data-props')); - - import('../mastodon/containers/admin_component').then(({ default: AdminComponent }) => { - return import('../mastodon/components/admin/' + componentName).then(({ default: Component }) => { - ReactDOM.render(( - - - - ), element); - }); - }).catch(error => { - console.error(error); - }); - }); }); diff --git a/app/javascript/flavours/glitch/components/admin/Counter.js b/app/javascript/flavours/glitch/components/admin/Counter.js new file mode 100644 index 000000000..39ef216bd --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/Counter.js @@ -0,0 +1,115 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import api from 'flavours/glitch/util/api'; +import { FormattedNumber } from 'react-intl'; +import { Sparklines, SparklinesCurve } from 'react-sparklines'; +import classNames from 'classnames'; +import Skeleton from 'flavours/glitch/components/skeleton'; + +const percIncrease = (a, b) => { + let percent; + + if (b !== 0) { + if (a !== 0) { + percent = (b - a) / a; + } else { + percent = 1; + } + } else if (b === 0 && a === 0) { + percent = 0; + } else { + percent = - 1; + } + + return percent; +}; + +export default class Counter extends React.PureComponent { + + static propTypes = { + measure: PropTypes.string.isRequired, + start_at: PropTypes.string.isRequired, + end_at: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + href: PropTypes.string, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { measure, start_at, end_at } = this.props; + + api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { label, href } = this.props; + const { loading, data } = this.state; + + let content; + + if (loading) { + content = ( + + + + + ); + } else { + const measure = data[0]; + const percentChange = percIncrease(measure.previous_total * 1, measure.total * 1); + + content = ( + + + 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'} + + ); + } + + const inner = ( + +
+ {content} +
+ +
+ {label} +
+ +
+ {!loading && ( + x.value * 1)}> + + + )} +
+
+ ); + + if (href) { + return ( + + {inner} + + ); + } else { + return ( +
+ {inner} +
+ ); + } + } + +} diff --git a/app/javascript/flavours/glitch/components/admin/Dimension.js b/app/javascript/flavours/glitch/components/admin/Dimension.js new file mode 100644 index 000000000..b4fbf86c8 --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/Dimension.js @@ -0,0 +1,92 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import api from 'flavours/glitch/util/api'; +import { FormattedNumber } from 'react-intl'; +import { roundTo10 } from 'flavours/glitch/util/numbers'; +import Skeleton from 'flavours/glitch/components/skeleton'; + +export default class Dimension extends React.PureComponent { + + static propTypes = { + dimension: PropTypes.string.isRequired, + start_at: PropTypes.string.isRequired, + end_at: PropTypes.string.isRequired, + limit: PropTypes.number.isRequired, + label: PropTypes.string.isRequired, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { start_at, end_at, dimension, limit } = this.props; + + api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { label, limit } = this.props; + const { loading, data } = this.state; + + let content; + + if (loading) { + content = ( + + + {Array.from(Array(limit)).map((_, i) => ( + + + + + + ))} + +
+ + + +
+ ); + } else { + const sum = data[0].data.reduce((sum, cur) => sum + (cur.value * 1), 0); + + content = ( + + + {data[0].data.map(item => ( + + + + + + ))} + +
+ + {item.human_key} + + {typeof item.human_value !== 'undefined' ? item.human_value : } +
+ ); + } + + return ( +
+

{label}

+ + {content} +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/admin/Retention.js b/app/javascript/flavours/glitch/components/admin/Retention.js new file mode 100644 index 000000000..8295362a4 --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/Retention.js @@ -0,0 +1,141 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import api from 'flavours/glitch/util/api'; +import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl'; +import classNames from 'classnames'; +import { roundTo10 } from 'flavours/glitch/util/numbers'; + +const dateForCohort = cohort => { + switch(cohort.frequency) { + case 'day': + return ; + default: + return ; + } +}; + +export default class Retention extends React.PureComponent { + + static propTypes = { + start_at: PropTypes.string, + end_at: PropTypes.string, + frequency: PropTypes.string, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { start_at, end_at, frequency } = this.props; + + api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { loading, data } = this.state; + + let content; + + if (loading) { + content = ; + } else { + content = ( + + + + + + + + {data[0].data.slice(1).map((retention, i) => ( + + ))} + + + + + + + + {data[0].data.slice(1).map((retention, i) => { + const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].percent - sum)/(k + 1) : sum, 0); + + return ( + + ); + })} + + + + + {data.slice(0, -1).map(cohort => ( + + + + + + {cohort.data.slice(1).map(retention => ( + + ))} + + ))} + +
+
+ +
+
+
+ +
+
+
+ {i + 1} +
+
+
+ +
+
+
+ sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} /> +
+
+
+ +
+
+
+ {dateForCohort(cohort)} +
+
+
+ +
+
+
+ +
+
+ ); + } + + return ( +
+

+ + {content} +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/admin/Trends.js b/app/javascript/flavours/glitch/components/admin/Trends.js new file mode 100644 index 000000000..d7c4eb72c --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/Trends.js @@ -0,0 +1,73 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import api from 'flavours/glitch/util/api'; +import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; +import Hashtag from 'flavours/glitch/components/hashtag'; + +export default class Trends extends React.PureComponent { + + static propTypes = { + limit: PropTypes.number.isRequired, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { limit } = this.props; + + api().get('/api/v1/admin/trends', { params: { limit } }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { limit } = this.props; + const { loading, data } = this.state; + + let content; + + if (loading) { + content = ( +
+ {Array.from(Array(limit)).map((_, i) => ( + + ))} +
+ ); + } else { + content = ( +
+ {data.map(hashtag => ( + day.uses)} + className={classNames(hashtag.requires_review && 'trends__item--requires-review', !hashtag.trendable && !hashtag.requires_review && 'trends__item--disabled')} + /> + ))} +
+ ); + } + + return ( +
+

+ + {content} +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.js index d00c01e77..769185a2b 100644 --- a/app/javascript/flavours/glitch/components/hashtag.js +++ b/app/javascript/flavours/glitch/components/hashtag.js @@ -6,6 +6,8 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Permalink from './permalink'; import ShortNumber from 'flavours/glitch/components/short_number'; +import Skeleton from 'flavours/glitch/components/skeleton'; +import classNames from 'classnames'; class SilentErrorBoundary extends React.Component { @@ -47,45 +49,38 @@ const accountsCountRenderer = (displayNumber, pluralReady) => ( /> ); -const Hashtag = ({ hashtag }) => ( -
+export const ImmutableHashtag = ({ hashtag }) => ( + day.get('uses')).toArray()} + /> +); + +ImmutableHashtag.propTypes = { + hashtag: ImmutablePropTypes.map.isRequired, +}; + +const Hashtag = ({ name, href, to, people, uses, history, className }) => ( +
- - #{hashtag.get('name')} + + {name ? #{name} : } - + {typeof people !== 'undefined' ? : }
- + {typeof uses !== 'undefined' ? : }
- day.get('uses')) - .toArray()} - > + 0)}> @@ -94,7 +89,13 @@ const Hashtag = ({ hashtag }) => ( ); Hashtag.propTypes = { - hashtag: ImmutablePropTypes.map.isRequired, + name: PropTypes.string, + href: PropTypes.string, + to: PropTypes.string, + people: PropTypes.number, + uses: PropTypes.number, + history: PropTypes.arrayOf(PropTypes.number), + className: PropTypes.string, }; export default Hashtag; diff --git a/app/javascript/flavours/glitch/components/skeleton.js b/app/javascript/flavours/glitch/components/skeleton.js new file mode 100644 index 000000000..09093e99c --- /dev/null +++ b/app/javascript/flavours/glitch/components/skeleton.js @@ -0,0 +1,11 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const Skeleton = ({ width, height }) => ; + +Skeleton.propTypes = { + width: PropTypes.number, + height: PropTypes.number, +}; + +export default Skeleton; diff --git a/app/javascript/flavours/glitch/containers/admin_component.js b/app/javascript/flavours/glitch/containers/admin_component.js new file mode 100644 index 000000000..64dabac8b --- /dev/null +++ b/app/javascript/flavours/glitch/containers/admin_component.js @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { getLocale } from 'mastodon/locales'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); + +export default class AdminComponent extends React.PureComponent { + + static propTypes = { + locale: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + }; + + render () { + const { locale, children } = this.props; + + return ( + + {children} + + ); + } + +} diff --git a/app/javascript/flavours/glitch/containers/media_container.js b/app/javascript/flavours/glitch/containers/media_container.js index 8657b8064..1ddbc706b 100644 --- a/app/javascript/flavours/glitch/containers/media_container.js +++ b/app/javascript/flavours/glitch/containers/media_container.js @@ -7,7 +7,7 @@ import { getLocale } from 'mastodon/locales'; import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar'; import MediaGallery from 'flavours/glitch/components/media_gallery'; import Poll from 'flavours/glitch/components/poll'; -import Hashtag from 'flavours/glitch/components/hashtag'; +import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; import ModalRoot from 'flavours/glitch/components/modal_root'; import MediaModal from 'flavours/glitch/features/ui/components/media_modal'; import Video from 'flavours/glitch/features/video'; diff --git a/app/javascript/flavours/glitch/features/compose/components/search_results.js b/app/javascript/flavours/glitch/features/compose/components/search_results.js index 9a76e5418..cbc1f35e5 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search_results.js +++ b/app/javascript/flavours/glitch/features/compose/components/search_results.js @@ -5,7 +5,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import AccountContainer from 'flavours/glitch/containers/account_container'; import StatusContainer from 'flavours/glitch/containers/status_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import Hashtag from 'flavours/glitch/components/hashtag'; +import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; import Icon from 'flavours/glitch/components/icon'; import { searchEnabled } from 'flavours/glitch/util/initial_state'; import LoadMore from 'flavours/glitch/components/load_more'; diff --git a/app/javascript/flavours/glitch/features/getting_started/components/trends.js b/app/javascript/flavours/glitch/features/getting_started/components/trends.js index 0734ec72b..ce4d94c64 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/trends.js +++ b/app/javascript/flavours/glitch/features/getting_started/components/trends.js @@ -2,7 +2,7 @@ import React from 'react'; import ImmutablePureComponent from 'react-immutable-pure-component'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import Hashtag from 'flavours/glitch/components/hashtag'; +import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; import { FormattedMessage } from 'react-intl'; export default class Trends extends ImmutablePureComponent { diff --git a/app/javascript/flavours/glitch/packs/admin.js b/app/javascript/flavours/glitch/packs/admin.js new file mode 100644 index 000000000..4c09ddb05 --- /dev/null +++ b/app/javascript/flavours/glitch/packs/admin.js @@ -0,0 +1,24 @@ +import 'packs/public-path'; +import ready from 'flavours/glitch/util/ready'; + +ready(() => { + const React = require('react'); + const ReactDOM = require('react-dom'); + + [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => { + const componentName = element.getAttribute('data-admin-component'); + const { locale, ...componentProps } = JSON.parse(element.getAttribute('data-props')); + + import('flavours/glitch/containers/admin_component').then(({ default: AdminComponent }) => { + return import('flavours/glitch/components/admin/' + componentName).then(({ default: Component }) => { + ReactDOM.render(( + + + + ), element); + }); + }).catch(error => { + console.error(error); + }); + }); +}); diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 4801a4644..24618c29f 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -1,3 +1,5 @@ +@use "sass:math"; + $no-columns-breakpoint: 600px; $sidebar-width: 240px; $content-width: 840px; @@ -925,10 +927,197 @@ a.name-tag, } } +.dashboard__counters.admin-account-counters { + margin-top: 10px; +} + .account-badges { margin: -2px 0; } -.dashboard__counters.admin-account-counters { - margin-top: 10px; +.retention { + &__table { + &__number { + color: $secondary-text-color; + padding: 10px; + } + + &__date { + white-space: nowrap; + padding: 10px 0; + text-align: left; + min-width: 120px; + + &.retention__table__average { + font-weight: 700; + } + } + + &__size { + text-align: center; + padding: 10px; + } + + &__label { + font-weight: 700; + color: $darker-text-color; + } + + &__box { + box-sizing: border-box; + background: $ui-highlight-color; + padding: 10px; + font-weight: 500; + color: $primary-text-color; + width: 52px; + margin: 1px; + + @for $i from 0 through 10 { + &--#{10 * $i} { + background-color: rgba($ui-highlight-color, 1 * (math.div(max(1, $i), 10))); + } + } + } + } +} + +.sparkline { + display: block; + text-decoration: none; + background: lighten($ui-base-color, 4%); + border-radius: 4px; + padding: 0; + position: relative; + padding-bottom: 55px + 20px; + overflow: hidden; + + &__value { + display: flex; + line-height: 33px; + align-items: flex-end; + padding: 20px; + padding-bottom: 10px; + + &__total { + display: block; + margin-right: 10px; + font-weight: 500; + font-size: 28px; + color: $primary-text-color; + } + + &__change { + display: block; + font-weight: 500; + font-size: 18px; + color: $darker-text-color; + margin-bottom: -3px; + + &.positive { + color: $valid-value-color; + } + + &.negative { + color: $error-value-color; + } + } + } + + &__label { + padding: 0 20px; + padding-bottom: 10px; + text-transform: uppercase; + color: $darker-text-color; + font-weight: 500; + } + + &__graph { + position: absolute; + bottom: 0; + + svg { + display: block; + margin: 0; + } + + path:first-child { + fill: rgba($highlight-text-color, 0.25) !important; + fill-opacity: 1 !important; + } + + path:last-child { + stroke: lighten($highlight-text-color, 6%) !important; + fill: none !important; + } + } +} + +a.sparkline { + &:hover, + &:focus, + &:active { + background: lighten($ui-base-color, 6%); + } +} + +.skeleton { + background-color: lighten($ui-base-color, 8%); + background-image: linear-gradient(90deg, lighten($ui-base-color, 8%), lighten($ui-base-color, 12%), lighten($ui-base-color, 8%)); + background-size: 200px 100%; + background-repeat: no-repeat; + border-radius: 4px; + display: inline-block; + line-height: 1; + width: 100%; + animation: skeleton 1.2s ease-in-out infinite; +} + +@keyframes skeleton { + 0% { + background-position: -200px 0; + } + + 100% { + background-position: calc(200px + 100%) 0; + } +} + +.dimension { + table { + width: 100%; + } + + &__item { + border-bottom: 1px solid lighten($ui-base-color, 4%); + + &__key { + font-weight: 500; + padding: 11px 10px; + } + + &__value { + text-align: right; + color: $darker-text-color; + padding: 11px 10px; + } + + &__indicator { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background: $ui-highlight-color; + margin-right: 10px; + + @for $i from 0 through 10 { + &--#{10 * $i} { + background-color: rgba($ui-highlight-color, 1 * (math.div(max(1, $i), 10))); + } + } + } + + &:last-child { + border-bottom: 0; + } + } } diff --git a/app/javascript/flavours/glitch/styles/components/search.scss b/app/javascript/flavours/glitch/styles/components/search.scss index 929769130..f7415368b 100644 --- a/app/javascript/flavours/glitch/styles/components/search.scss +++ b/app/javascript/flavours/glitch/styles/components/search.scss @@ -171,7 +171,6 @@ &__current { flex: 0 0 auto; font-size: 24px; - line-height: 36px; font-weight: 500; text-align: right; padding-right: 15px; @@ -193,5 +192,57 @@ fill: none !important; } } + + &--requires-review { + .trends__item__name { + color: $gold-star; + + a { + color: $gold-star; + } + } + + .trends__item__current { + color: $gold-star; + } + + .trends__item__sparkline { + path:first-child { + fill: rgba($gold-star, 0.25) !important; + } + + path:last-child { + stroke: lighten($gold-star, 6%) !important; + } + } + } + + &--disabled { + .trends__item__name { + color: lighten($ui-base-color, 12%); + + a { + color: lighten($ui-base-color, 12%); + } + } + + .trends__item__current { + color: lighten($ui-base-color, 12%); + } + + .trends__item__sparkline { + path:first-child { + fill: rgba(lighten($ui-base-color, 12%), 0.25) !important; + } + + path:last-child { + stroke: lighten(lighten($ui-base-color, 12%), 6%) !important; + } + } + } + } + + &--compact &__item { + padding: 10px; } } diff --git a/app/javascript/flavours/glitch/styles/dashboard.scss b/app/javascript/flavours/glitch/styles/dashboard.scss index c0944d417..cad5a105b 100644 --- a/app/javascript/flavours/glitch/styles/dashboard.scss +++ b/app/javascript/flavours/glitch/styles/dashboard.scss @@ -56,23 +56,56 @@ } } -.dashboard__widgets { - display: flex; - flex-wrap: wrap; - margin: 0 -5px; +.dashboard { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); + grid-gap: 10px; - & > div { - flex: 0 0 33.333%; - margin-bottom: 20px; + &__item { + &--span-double-column { + grid-column: span 2; + } - & > div { - padding: 0 5px; + &--span-double-row { + grid-row: span 2; + } + + h4 { + padding-top: 20px; } } - a:not(.name-tag) { - color: $ui-secondary-color; - font-weight: 500; + &__quick-access { + display: flex; + align-items: baseline; + border-radius: 4px; + background: $ui-highlight-color; + color: $primary-text-color; + transition: all 100ms ease-in; + font-size: 14px; + padding: 0 16px; + line-height: 36px; + height: 36px; text-decoration: none; + margin-bottom: 4px; + + &:active, + &:focus, + &:hover { + background-color: lighten($ui-highlight-color, 10%); + transition: all 200ms ease-out; + } + + span { + flex: 1 1 auto; + } + + .fa { + flex: 0 0 auto; + } + + strong { + font-weight: 700; + } } } diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml index 2a98e4c29..ee2b699b2 100644 --- a/app/javascript/flavours/glitch/theme.yml +++ b/app/javascript/flavours/glitch/theme.yml @@ -1,7 +1,7 @@ # (REQUIRED) The location of the pack files. pack: about: packs/about.js - admin: packs/public.js + admin: packs/admin.js auth: packs/public.js common: filename: packs/common.js diff --git a/app/javascript/flavours/glitch/util/numbers.js b/app/javascript/flavours/glitch/util/numbers.js index 6f2505cae..6ef563ad8 100644 --- a/app/javascript/flavours/glitch/util/numbers.js +++ b/app/javascript/flavours/glitch/util/numbers.js @@ -69,3 +69,11 @@ export function pluralReady(sourceNumber, division) { return Math.trunc(sourceNumber / closestScale) * closestScale; } + +/** + * @param {number} num + * @returns {number} + */ +export function roundTo10(num) { + return Math.round(num * 0.1) / 0.1; +} diff --git a/app/javascript/flavours/vanilla/theme.yml b/app/javascript/flavours/vanilla/theme.yml index 74e9fb1b5..3263fd7d4 100644 --- a/app/javascript/flavours/vanilla/theme.yml +++ b/app/javascript/flavours/vanilla/theme.yml @@ -1,7 +1,7 @@ # (REQUIRED) The location of the pack files inside `pack_directory`. pack: about: about.js - admin: public.js + admin: admin.js auth: public.js common: filename: common.js diff --git a/app/javascript/packs/admin.js b/app/javascript/packs/admin.js new file mode 100644 index 000000000..599015000 --- /dev/null +++ b/app/javascript/packs/admin.js @@ -0,0 +1,24 @@ +import './public-path'; +import ready from '../mastodon/ready'; + +ready(() => { + const React = require('react'); + const ReactDOM = require('react-dom'); + + [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => { + const componentName = element.getAttribute('data-admin-component'); + const { locale, ...componentProps } = JSON.parse(element.getAttribute('data-props')); + + import('../mastodon/containers/admin_component').then(({ default: AdminComponent }) => { + return import('../mastodon/components/admin/' + componentName).then(({ default: Component }) => { + ReactDOM.render(( + + + + ), element); + }); + }).catch(error => { + console.error(error); + }); + }); +}); -- cgit From dbbb077c190a59f752b2a345df93cea010e464c8 Mon Sep 17 00:00:00 2001 From: Mashiro Date: Thu, 21 Oct 2021 12:24:34 +0800 Subject: [Glitch] Enhance dashboard styles Port b58d32cfe259d95ef28a61cbd863336350f2a3d9 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/admin.scss | 8 ++++++++ app/javascript/flavours/glitch/styles/dashboard.scss | 4 ++++ 2 files changed, 12 insertions(+) (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 24618c29f..bfb09aa0a 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -936,6 +936,13 @@ a.name-tag, } .retention { + overflow: auto; + + > h4 { + position: sticky; + left: 0; + } + &__table { &__number { color: $secondary-text-color; @@ -1034,6 +1041,7 @@ a.name-tag, &__graph { position: absolute; bottom: 0; + width: 100%; svg { display: block; diff --git a/app/javascript/flavours/glitch/styles/dashboard.scss b/app/javascript/flavours/glitch/styles/dashboard.scss index cad5a105b..5e900e8c5 100644 --- a/app/javascript/flavours/glitch/styles/dashboard.scss +++ b/app/javascript/flavours/glitch/styles/dashboard.scss @@ -61,6 +61,10 @@ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); grid-gap: 10px; + @media screen and (max-width: 1350px) { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + } + &__item { &--span-double-column { grid-column: span 2; -- cgit From f33878969a7fa38f4275c99b8970f9a567aed9d3 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 12 Nov 2021 15:45:36 +0100 Subject: Fix some glitch-soc styling issues due to different class names Partial fix to #1629 --- app/javascript/flavours/glitch/styles/containers.scss | 2 +- app/javascript/flavours/glitch/styles/contrast/diff.scss | 13 ------------- app/javascript/flavours/glitch/styles/rtl.scss | 2 +- 3 files changed, 2 insertions(+), 15 deletions(-) (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index 63374f3c3..eb72eab28 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -44,7 +44,7 @@ } .compose-standalone { - .compose-form { + .composer { width: 400px; margin: 0 auto; padding: 20px 0; diff --git a/app/javascript/flavours/glitch/styles/contrast/diff.scss b/app/javascript/flavours/glitch/styles/contrast/diff.scss index 0f3a6cc6d..9bd31cd7e 100644 --- a/app/javascript/flavours/glitch/styles/contrast/diff.scss +++ b/app/javascript/flavours/glitch/styles/contrast/diff.scss @@ -1,17 +1,4 @@ // components.scss -.compose-form { - .compose-form__modifiers { - .compose-form__upload { - &-description { - input { - &::placeholder { - opacity: 1.0; - } - } - } - } - } -} .rich-formatting a, .rich-formatting p a, diff --git a/app/javascript/flavours/glitch/styles/rtl.scss b/app/javascript/flavours/glitch/styles/rtl.scss index 61e95ad24..afa05d93e 100644 --- a/app/javascript/flavours/glitch/styles/rtl.scss +++ b/app/javascript/flavours/glitch/styles/rtl.scss @@ -51,7 +51,7 @@ body.rtl { margin-left: 5px; } - .compose-form .compose-form__buttons-wrapper .character-counter__wrapper { + .composer .compose--counter-wrapper { margin-right: 0; margin-left: 4px; } -- cgit From 189cf652e6b3c6ee1d303282ca93823965f89f24 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 19 Nov 2021 18:22:49 +0100 Subject: [Glitch] Fix overflow of long profile fields in admin view Port db32835338e113f23a474d323e398916a999619f to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/admin.scss | 1 + 1 file changed, 1 insertion(+) (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index bfb09aa0a..8fd556c73 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -847,6 +847,7 @@ a.name-tag, padding: 0 5px; margin-bottom: 10px; flex: 1 0 50%; + max-width: 100%; } .account__header__fields, -- cgit From 97151840b02499a0cec1360907a6b86a1df02b3b Mon Sep 17 00:00:00 2001 From: Jeong Arm Date: Fri, 26 Nov 2021 15:01:04 +0900 Subject: [Glitch] Port upstream changes about trending links --- .../flavours/glitch/components/admin/Counter.js | 5 +++-- .../flavours/glitch/components/admin/Dimension.js | 5 +++-- .../flavours/glitch/components/admin/Trends.js | 2 +- app/javascript/flavours/glitch/styles/accounts.scss | 16 ++++++++++++++++ app/javascript/flavours/glitch/styles/dashboard.scss | 10 ++++++++++ 5 files changed, 33 insertions(+), 5 deletions(-) (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/flavours/glitch/components/admin/Counter.js b/app/javascript/flavours/glitch/components/admin/Counter.js index 39ef216bd..2bc9ce482 100644 --- a/app/javascript/flavours/glitch/components/admin/Counter.js +++ b/app/javascript/flavours/glitch/components/admin/Counter.js @@ -32,6 +32,7 @@ export default class Counter extends React.PureComponent { end_at: PropTypes.string.isRequired, label: PropTypes.string.isRequired, href: PropTypes.string, + params: PropTypes.object, }; state = { @@ -40,9 +41,9 @@ export default class Counter extends React.PureComponent { }; componentDidMount () { - const { measure, start_at, end_at } = this.props; + const { measure, start_at, end_at, params } = this.props; - api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at }).then(res => { + api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => { this.setState({ loading: false, data: res.data, diff --git a/app/javascript/flavours/glitch/components/admin/Dimension.js b/app/javascript/flavours/glitch/components/admin/Dimension.js index b4fbf86c8..a924d093c 100644 --- a/app/javascript/flavours/glitch/components/admin/Dimension.js +++ b/app/javascript/flavours/glitch/components/admin/Dimension.js @@ -13,6 +13,7 @@ export default class Dimension extends React.PureComponent { end_at: PropTypes.string.isRequired, limit: PropTypes.number.isRequired, label: PropTypes.string.isRequired, + params: PropTypes.object, }; state = { @@ -21,9 +22,9 @@ export default class Dimension extends React.PureComponent { }; componentDidMount () { - const { start_at, end_at, dimension, limit } = this.props; + const { start_at, end_at, dimension, limit, params } = this.props; - api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit }).then(res => { + api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => { this.setState({ loading: false, data: res.data, diff --git a/app/javascript/flavours/glitch/components/admin/Trends.js b/app/javascript/flavours/glitch/components/admin/Trends.js index d7c4eb72c..60e367f00 100644 --- a/app/javascript/flavours/glitch/components/admin/Trends.js +++ b/app/javascript/flavours/glitch/components/admin/Trends.js @@ -19,7 +19,7 @@ export default class Trends extends React.PureComponent { componentDidMount () { const { limit } = this.props; - api().get('/api/v1/admin/trends', { params: { limit } }).then(res => { + api().get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => { this.setState({ loading: false, data: res.data, diff --git a/app/javascript/flavours/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss index a5ddde937..fe7dfc20f 100644 --- a/app/javascript/flavours/glitch/styles/accounts.scss +++ b/app/javascript/flavours/glitch/styles/accounts.scss @@ -327,3 +327,19 @@ margin-top: 10px; } } + +.batch-table__row--muted .pending-account__header { + &, + a, + strong { + color: lighten($ui-base-color, 26%); + } +} + +.batch-table__row--attention .pending-account__header { + &, + a, + strong { + color: $gold-star; + } +} diff --git a/app/javascript/flavours/glitch/styles/dashboard.scss b/app/javascript/flavours/glitch/styles/dashboard.scss index 5e900e8c5..0a881bc10 100644 --- a/app/javascript/flavours/glitch/styles/dashboard.scss +++ b/app/javascript/flavours/glitch/styles/dashboard.scss @@ -100,6 +100,16 @@ transition: all 200ms ease-out; } + &.positive { + background: lighten($ui-base-color, 4%); + color: $valid-value-color; + } + + &.negative { + background: lighten($ui-base-color, 4%); + color: $error-value-color; + } + span { flex: 1 1 auto; } -- cgit From d5d44b431a8c1570e5db1c5a894a0b72131a3573 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 26 Nov 2021 22:09:11 +0100 Subject: [Glitch] Fix color of hashtag column settings inputs Port 1630807ee2517e7a9dbb66cbd532a0c46e01abcf to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/components/columns.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index 04d9b4168..512a04376 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -728,7 +728,8 @@ } &__multi-value__label, - &__input { + &__input, + &__input-container { color: $darker-text-color; } -- cgit From d911c17f521d6b13861caa886715a50b644007a1 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Dec 2021 19:13:43 +0100 Subject: Fix unneeded outline around list name edition input --- app/javascript/flavours/glitch/styles/components/index.scss | 1 + 1 file changed, 1 insertion(+) (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 2281a4bb3..2656890d7 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -1040,6 +1040,7 @@ background: transparent; border: 0; border-bottom: 2px solid $ui-primary-color; + outline: 0; box-sizing: border-box; display: block; font-family: inherit; -- cgit From 9cecf59300a0744f00957b9dbdb05572f5e5dbdd Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 5 Dec 2021 21:48:39 +0100 Subject: [Glitch] Add batch suspend for accounts in admin UI Port SCSS changes from 2aafa5b4e7a83ce8195cd739f1233a52ab060db7 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/styles/accounts.scss | 30 ++++++++++++++++++++-- app/javascript/flavours/glitch/styles/tables.scss | 5 ++++ app/javascript/flavours/glitch/styles/widgets.scss | 18 +++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/flavours/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss index fe7dfc20f..56b143fe6 100644 --- a/app/javascript/flavours/glitch/styles/accounts.scss +++ b/app/javascript/flavours/glitch/styles/accounts.scss @@ -328,7 +328,12 @@ } } -.batch-table__row--muted .pending-account__header { +.batch-table__row--muted { + color: lighten($ui-base-color, 26%); +} + +.batch-table__row--muted .pending-account__header, +.batch-table__row--muted .accounts-table { &, a, strong { @@ -336,10 +341,31 @@ } } -.batch-table__row--attention .pending-account__header { +.batch-table__row--muted .accounts-table { + tbody td.accounts-table__extra, + &__count, + &__count small { + color: lighten($ui-base-color, 26%); + } +} + +.batch-table__row--attention { + color: $gold-star; +} + +.batch-table__row--attention .pending-account__header, +.batch-table__row--attention .accounts-table { &, a, strong { color: $gold-star; } } + +.batch-table__row--attention .accounts-table { + tbody td.accounts-table__extra, + &__count, + &__count small { + color: $gold-star; + } +} diff --git a/app/javascript/flavours/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss index ec2ee7c1c..12c84a6c9 100644 --- a/app/javascript/flavours/glitch/styles/tables.scss +++ b/app/javascript/flavours/glitch/styles/tables.scss @@ -237,6 +237,11 @@ a.table-action-link { flex: 1 1 auto; } + &__quote { + padding: 12px; + padding-top: 0; + } + &__extra { flex: 0 0 auto; text-align: right; diff --git a/app/javascript/flavours/glitch/styles/widgets.scss b/app/javascript/flavours/glitch/styles/widgets.scss index 06bf55e1e..a88f3b2c7 100644 --- a/app/javascript/flavours/glitch/styles/widgets.scss +++ b/app/javascript/flavours/glitch/styles/widgets.scss @@ -434,6 +434,24 @@ } } + tbody td.accounts-table__extra { + width: 120px; + text-align: right; + color: $darker-text-color; + padding-right: 16px; + + a { + text-decoration: none; + color: inherit; + + &:focus, + &:hover, + &:active { + text-decoration: underline; + } + } + } + &__comment { width: 50%; vertical-align: initial !important; -- cgit From 69208ef6ff16860a885eb389b3d14afd93531252 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 17 Jan 2022 09:41:33 +0100 Subject: [Glitch] Add notifications for statuses deleted by moderators Port front-end changes from 14f436c457560862fafabd753eb314c8b8a8e674 to glitch-soc Signed-off-by: Claire --- .../components/admin/ReportReasonSelector.js | 159 ++++++++++ app/javascript/flavours/glitch/styles/admin.scss | 328 ++++++++++++++++++++- app/javascript/flavours/glitch/styles/polls.scss | 15 + .../flavours/glitch/util/backend_links.js | 2 +- 4 files changed, 488 insertions(+), 16 deletions(-) create mode 100644 app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js new file mode 100644 index 000000000..0f2a4fe36 --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js @@ -0,0 +1,159 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import api from 'flavours/glitch/util/api'; +import { injectIntl, defineMessages } from 'react-intl'; +import classNames from 'classnames'; + +const messages = defineMessages({ + other: { id: 'report.categories.other', defaultMessage: 'Other' }, + spam: { id: 'report.categories.spam', defaultMessage: 'Spam' }, + violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' }, +}); + +class Category extends React.PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + selected: PropTypes.bool, + disabled: PropTypes.bool, + onSelect: PropTypes.func, + children: PropTypes.node, + }; + + handleClick = () => { + const { id, disabled, onSelect } = this.props; + + if (!disabled) { + onSelect(id); + } + }; + + render () { + const { id, text, disabled, selected, children } = this.props; + + return ( +
+ {selected && } + +
+ + {text} +
+ + {(selected && children) && ( +
+ {children} +
+ )} +
+ ); + } + +} + +class Rule extends React.PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + selected: PropTypes.bool, + disabled: PropTypes.bool, + onToggle: PropTypes.func, + }; + + handleClick = () => { + const { id, disabled, onToggle } = this.props; + + if (!disabled) { + onToggle(id); + } + }; + + render () { + const { id, text, disabled, selected } = this.props; + + return ( +
+ + {selected && } + {text} +
+ ); + } + +} + +export default @injectIntl +class ReportReasonSelector extends React.PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + category: PropTypes.string.isRequired, + rule_ids: PropTypes.arrayOf(PropTypes.string), + disabled: PropTypes.bool, + intl: PropTypes.object.isRequired, + }; + + state = { + category: this.props.category, + rule_ids: this.props.rule_ids || [], + rules: [], + }; + + componentDidMount() { + api().get('/api/v1/instance').then(res => { + this.setState({ + rules: res.data.rules, + }); + }).catch(err => { + console.error(err); + }); + } + + _save = () => { + const { id, disabled } = this.props; + const { category, rule_ids } = this.state; + + if (disabled) { + return; + } + + api().put(`/api/v1/admin/reports/${id}`, { + category, + rule_ids, + }).catch(err => { + console.error(err); + }); + }; + + handleSelect = id => { + this.setState({ category: id }, () => this._save()); + }; + + handleToggle = id => { + const { rule_ids } = this.state; + + if (rule_ids.includes(id)) { + this.setState({ rule_ids: rule_ids.filter(x => x !== id ) }, () => this._save()); + } else { + this.setState({ rule_ids: [...rule_ids, id] }, () => this._save()); + } + }; + + render () { + const { disabled, intl } = this.props; + const { rules, category, rule_ids } = this.state; + + return ( +
+ + + + {rules.map(rule => )} + +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 8fd556c73..92061585a 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -595,39 +595,44 @@ body, .log-entry { line-height: 20px; - padding: 15px 0; + padding: 15px; + padding-left: 15px * 2 + 40px; background: $ui-base-color; - border-bottom: 1px solid lighten($ui-base-color, 4%); + border-bottom: 1px solid darken($ui-base-color, 8%); + position: relative; + + &:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } &:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; border-bottom: 0; } + &:hover { + background: lighten($ui-base-color, 4%); + } + &__header { - display: flex; - justify-content: flex-start; - align-items: center; color: $darker-text-color; font-size: 14px; - padding: 0 10px; } &__avatar { - margin-right: 10px; + position: absolute; + left: 15px; + top: 15px; .avatar { - display: block; - margin: 0; - border-radius: 50%; + border-radius: 4px; width: 40px; height: 40px; } } - &__content { - max-width: calc(100% - 90px); - } - &__title { word-wrap: break-word; } @@ -643,6 +648,14 @@ body, text-decoration: none; font-weight: 500; } + + a { + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } } a.name-tag, @@ -671,8 +684,9 @@ a.inline-name-tag, a.name-tag, .name-tag { - display: flex; + display: inline-flex; align-items: center; + vertical-align: top; .avatar { display: block; @@ -1130,3 +1144,287 @@ a.sparkline { } } } + +.report-reason-selector { + border-radius: 4px; + background: $ui-base-color; + margin-bottom: 20px; + + &__category { + cursor: pointer; + border-bottom: 1px solid darken($ui-base-color, 8%); + + &:last-child { + border-bottom: 0; + } + + &__label { + padding: 15px; + } + + &__rules { + margin-left: 30px; + } + } + + &__rule { + cursor: pointer; + padding: 15px; + } +} + +.report-header { + display: grid; + grid-gap: 15px; + grid-template-columns: minmax(0, 1fr) 300px; + + &__details { + &__item { + border-bottom: 1px solid lighten($ui-base-color, 8%); + padding: 15px 0; + + &:last-child { + border-bottom: 0; + } + + &__header { + font-weight: 600; + padding: 4px 0; + } + } + + &--horizontal { + display: grid; + grid-auto-columns: minmax(0, 1fr); + grid-auto-flow: column; + + .report-header__details__item { + border-bottom: 0; + } + } + } +} + +.account-card { + background: $ui-base-color; + border-radius: 4px; + + &__header { + padding: 4px; + border-radius: 4px; + height: 128px; + + img { + display: block; + margin: 0; + width: 100%; + height: 100%; + object-fit: cover; + background: darken($ui-base-color, 8%); + } + } + + &__title { + margin-top: -25px; + display: flex; + align-items: flex-end; + + &__avatar { + padding: 15px; + + img { + display: block; + margin: 0; + width: 56px; + height: 56px; + background: darken($ui-base-color, 8%); + border-radius: 8px; + } + } + + .display-name { + color: $darker-text-color; + padding-bottom: 15px; + font-size: 15px; + + bdi { + display: block; + color: $primary-text-color; + font-weight: 500; + } + } + } + + &__bio { + padding: 0 15px; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + max-height: 18px * 2; + position: relative; + + &::after { + display: block; + content: ""; + width: 50px; + height: 18px; + position: absolute; + bottom: 0; + right: 15px; + background: linear-gradient(to left, $ui-base-color, transparent); + pointer-events: none; + } + } + + &__actions { + display: flex; + align-items: center; + padding-top: 10px; + + &__button { + flex: 0 0 auto; + padding: 0 15px; + } + } + + &__counters { + flex: 1 1 auto; + display: grid; + grid-auto-columns: minmax(0, 1fr); + grid-auto-flow: column; + + &__item { + padding: 15px; + text-align: center; + color: $primary-text-color; + font-weight: 600; + font-size: 15px; + + small { + display: block; + color: $darker-text-color; + font-weight: 400; + font-size: 13px; + } + } + } +} + +.report-notes { + margin-bottom: 20px; + + &__item { + background: $ui-base-color; + position: relative; + padding: 15px; + padding-left: 15px * 2 + 40px; + border-bottom: 1px solid darken($ui-base-color, 8%); + + &:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } + + &:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: 0; + } + + &:hover { + background-color: lighten($ui-base-color, 4%); + } + + &__avatar { + position: absolute; + left: 15px; + top: 15px; + border-radius: 4px; + width: 40px; + height: 40px; + } + + &__header { + color: $darker-text-color; + font-size: 15px; + line-height: 20px; + margin-bottom: 4px; + + .username a { + color: $primary-text-color; + font-weight: 500; + text-decoration: none; + margin-right: 5px; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + + time { + margin-left: 5px; + vertical-align: baseline; + } + } + + &__content { + font-size: 15px; + line-height: 20px; + word-wrap: break-word; + font-weight: 400; + color: $primary-text-color; + + p { + margin-bottom: 20px; + white-space: pre-wrap; + unicode-bidi: plaintext; + + &:last-child { + margin-bottom: 0; + } + } + } + + &__actions { + position: absolute; + top: 15px; + right: 15px; + text-align: right; + } + } +} + +.report-actions { + border: 1px solid darken($ui-base-color, 8%); + + &__item { + display: flex; + align-items: center; + line-height: 18px; + border-bottom: 1px solid darken($ui-base-color, 8%); + + &:last-child { + border-bottom: 0; + } + + &__button { + flex: 0 0 auto; + width: 100px; + padding: 15px; + padding-right: 0; + + .button { + display: block; + width: 100%; + } + } + + &__description { + padding: 15px; + font-size: 14px; + color: $dark-text-color; + } + } +} diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss index 5fc41ed9e..a2cdecf06 100644 --- a/app/javascript/flavours/glitch/styles/polls.scss +++ b/app/javascript/flavours/glitch/styles/polls.scss @@ -150,6 +150,21 @@ &:active { outline: 0 !important; } + + &.disabled { + border-color: $dark-text-color; + + &.active { + background: $dark-text-color; + } + + &:active, + &:focus, + &:hover { + border-color: $dark-text-color; + border-width: 1px; + } + } } &__number { diff --git a/app/javascript/flavours/glitch/util/backend_links.js b/app/javascript/flavours/glitch/util/backend_links.js index 0fb378cc1..2e5111a7f 100644 --- a/app/javascript/flavours/glitch/util/backend_links.js +++ b/app/javascript/flavours/glitch/util/backend_links.js @@ -3,7 +3,7 @@ export const profileLink = '/settings/profile'; export const signOutLink = '/auth/sign_out'; export const termsLink = '/terms'; export const accountAdminLink = (id) => `/admin/accounts/${id}`; -export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses/${status_id}`; +export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses?id=${status_id}`; export const filterEditLink = (id) => `/filters/${id}/edit`; export const relationshipsLink = '/relationships'; export const securityLink = '/auth/edit'; -- cgit From d4654dc8929e4ded74194c95f8ee45daf6dc6516 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 19 Jan 2022 22:37:27 +0100 Subject: [Glitch] Add support for editing for published statuses Port front-end changes from 1060666c583670bb3b89ed5154e61038331e30c3 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/actions/importer/normalizer.js | 7 ++++--- app/javascript/flavours/glitch/actions/statuses.js | 3 +++ app/javascript/flavours/glitch/actions/streaming.js | 4 ++++ .../flavours/glitch/components/status_action_bar.js | 5 ++++- .../features/status/components/detailed_status.js | 20 ++++++++++++++++---- .../flavours/glitch/styles/components/status.scss | 11 +++++++++++ 6 files changed, 42 insertions(+), 8 deletions(-) (limited to 'app/javascript/flavours/glitch/styles') diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 3995585f6..bda15a9b0 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -54,9 +54,10 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.poll = status.poll.id; } - // Only calculate these values when status first encountered - // Otherwise keep the ones already in the reducer - if (normalOldStatus) { + // Only calculate these values when status first encountered and + // when the underlying values change. Otherwise keep the ones + // already in the reducer + if (normalOldStatus && normalOldStatus.get('content') === normalStatus.content && normalOldStatus.get('spoiler_text') === normalStatus.spoiler_text) { normalStatus.search_index = normalOldStatus.get('search_index'); normalStatus.contentHtml = normalOldStatus.get('contentHtml'); normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 4d2bda78b..7db357df1 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -128,6 +128,9 @@ export function deleteStatusFail(id, error) { }; }; +export const updateStatus = status => dispatch => + dispatch(importFetchedStatus(status)); + export function fetchContext(id) { return (dispatch, getState) => { dispatch(fetchContextRequest(id)); diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index 35db5dcc9..223924534 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -10,6 +10,7 @@ import { } from './timelines'; import { updateNotifications, expandNotifications } from './notifications'; import { updateConversations } from './conversations'; +import { updateStatus } from './statuses'; import { fetchAnnouncements, updateAnnouncements, @@ -75,6 +76,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti case 'update': dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept)); break; + case 'status.update': + dispatch(updateStatus(JSON.parse(data.payload))); + break; case 'delete': dispatch(deleteFromTimelines(data.payload)); break; diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 650b33b62..ae67c6116 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -38,6 +38,7 @@ const messages = defineMessages({ admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, hide: { id: 'status.hide', defaultMessage: 'Hide toot' }, + edited: { id: 'status.edited', defaultMessage: 'Edited {date}' }, }); export default @injectIntl @@ -324,7 +325,9 @@ class StatusActionBar extends ImmutablePureComponent {
, ]} - + + {status.get('edited_at') && *} +
); } diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index a1f981484..4b3a6aaaa 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -7,7 +7,7 @@ import StatusContent from 'flavours/glitch/components/status_content'; import MediaGallery from 'flavours/glitch/components/media_gallery'; import AttachmentList from 'flavours/glitch/components/attachment_list'; import { Link } from 'react-router-dom'; -import { FormattedDate } from 'react-intl'; +import { injectIntl, FormattedDate, FormattedMessage } from 'react-intl'; import Card from './card'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Video from 'flavours/glitch/features/video'; @@ -20,7 +20,8 @@ import Icon from 'flavours/glitch/components/icon'; import AnimatedNumber from 'flavours/glitch/components/animated_number'; import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; -export default class DetailedStatus extends ImmutablePureComponent { +export default @injectIntl +class DetailedStatus extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, @@ -40,6 +41,7 @@ export default class DetailedStatus extends ImmutablePureComponent { showMedia: PropTypes.bool, usingPiP: PropTypes.bool, onToggleMediaVisibility: PropTypes.func, + intl: PropTypes.object.isRequired, }; state = { @@ -111,7 +113,7 @@ export default class DetailedStatus extends ImmutablePureComponent { render () { const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; - const { expanded, onToggleHidden, settings, usingPiP } = this.props; + const { expanded, onToggleHidden, settings, usingPiP, intl } = this.props; const outerStyle = { boxSizing: 'border-box' }; const { compact } = this.props; @@ -125,6 +127,7 @@ export default class DetailedStatus extends ImmutablePureComponent { let reblogLink = ''; let reblogIcon = 'retweet'; let favouriteLink = ''; + let edited = ''; if (this.props.measureHeight) { outerStyle.height = `${this.state.height}px`; @@ -258,6 +261,15 @@ export default class DetailedStatus extends ImmutablePureComponent { ); } + if (status.get('edited_at')) { + edited = ( + + · + + + ); + } + return (
@@ -283,7 +295,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
- {visibilityLink}{applicationLink}{reblogLink} · {favouriteLink} + {edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index e9d30544f..013b1bd25 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -205,6 +205,17 @@ } } +.status__content__edited-label { + display: block; + cursor: default; + font-size: 15px; + line-height: 20px; + padding: 0; + padding-top: 8px; + color: $dark-text-color; + font-weight: 500; +} + .status__content__spoiler-link { display: inline-block; border-radius: 2px; -- cgit From 1b493c9fee954b5bd4c4b00f9f945a5d97e2d699 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 24 Jan 2022 19:06:19 +0100 Subject: Add optional hCaptcha support Fixes #1649 This requires setting `HCAPTCHA_SECRET_KEY` and `HCAPTCHA_SITE_KEY`, then enabling the admin setting at `/admin/settings/edit#form_admin_settings_captcha_enabled` Subsequently, a hCaptcha widget will be displayed on `/about` and `/auth/sign_up` unless: - the user is already signed-up already - the user has used an invite link - the user has already solved the captcha (and registration failed for another reason) The Content-Security-Policy headers are altered automatically to allow the third-party hCaptcha scripts on `/about` and `/auth/sign_up` following the same rules as above. --- .env.production.sample | 4 ++ Gemfile | 2 + Gemfile.lock | 3 ++ app/controllers/about_controller.rb | 2 + app/controllers/api/v1/accounts_controller.rb | 4 +- app/controllers/auth/registrations_controller.rb | 17 ++++++ app/controllers/concerns/captcha_concern.rb | 66 ++++++++++++++++++++++++ app/helpers/admin/settings_helper.rb | 4 ++ app/javascript/flavours/glitch/styles/forms.scss | 4 ++ app/models/form/admin_settings.rb | 2 + app/views/about/_registration.html.haml | 3 ++ app/views/admin/settings/edit.html.haml | 5 +- app/views/auth/registrations/new.html.haml | 3 ++ config/locales-glitch/en.yml | 3 ++ config/settings.yml | 1 + 15 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 app/controllers/concerns/captcha_concern.rb (limited to 'app/javascript/flavours/glitch/styles') diff --git a/.env.production.sample b/.env.production.sample index 13e89b40d..7de5e00f4 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -285,3 +285,7 @@ MAX_POLL_OPTION_CHARS=100 # Units are in bytes MAX_EMOJI_SIZE=51200 MAX_REMOTE_EMOJI_SIZE=204800 + +# Optional hCaptcha support +# HCAPTCHA_SECRET_KEY= +# HCAPTCHA_SITE_KEY= diff --git a/Gemfile b/Gemfile index eae5f11b7..282ab65e4 100644 --- a/Gemfile +++ b/Gemfile @@ -156,3 +156,5 @@ gem 'concurrent-ruby', require: false gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' + +gem "hcaptcha", "~> 7.1" diff --git a/Gemfile.lock b/Gemfile.lock index 8d72732eb..cc9a53e41 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -271,6 +271,8 @@ GEM railties (>= 4.0.1) hashdiff (1.0.1) hashie (4.1.0) + hcaptcha (7.1.0) + json highline (2.0.3) hiredis (0.6.3) hkdf (0.3.0) @@ -719,6 +721,7 @@ DEPENDENCIES fog-openstack (~> 0.3) fuubar (~> 2.5) hamlit-rails (~> 0.2) + hcaptcha (~> 7.1) hiredis (~> 0.6) htmlentities (~> 4.3) http (~> 5.0) diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 620c0ff78..5a35dbbcb 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -2,6 +2,7 @@ class AboutController < ApplicationController include RegistrationSpamConcern + include CaptchaConcern before_action :set_pack @@ -12,6 +13,7 @@ class AboutController < ApplicationController before_action :set_instance_presenter before_action :set_expires_in, only: [:more, :terms] before_action :set_registration_form_time, only: :show + before_action :extend_csp_for_captcha!, only: :show skip_before_action :require_functional!, only: [:more, :terms] diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 5c47158e0..8916c3f96 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Api::V1::AccountsController < Api::BaseController + include CaptchaConcern + before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :remove_from_followers, :block, :unblock, :mute, :unmute] before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers] before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute] @@ -83,7 +85,7 @@ class Api::V1::AccountsController < Api::BaseController end def check_enabled_registrations - forbidden if single_user_mode? || omniauth_only? || !allowed_registrations? + forbidden if single_user_mode? || omniauth_only? || !allowed_registrations? || captcha_enabled? end def allowed_registrations? diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 6b1f3fa82..3c9b38a4b 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -2,6 +2,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController include RegistrationSpamConcern + include CaptchaConcern layout :determine_layout @@ -15,6 +16,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController before_action :require_not_suspended!, only: [:update] before_action :set_cache_headers, only: [:edit, :update] before_action :set_registration_form_time, only: :new + before_action :extend_csp_for_captcha!, only: [:new, :create] + before_action :check_captcha!, only: :create skip_before_action :require_functional!, only: [:edit, :update] @@ -135,4 +138,18 @@ class Auth::RegistrationsController < Devise::RegistrationsController def set_cache_headers response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate' end + + def sign_up(resource_name, resource) + clear_captcha! + super + end + + def check_captcha! + super do |error| + build_resource(sign_up_params) + resource.validate + resource.errors.add(:base, error) + respond_with resource + end + end end diff --git a/app/controllers/concerns/captcha_concern.rb b/app/controllers/concerns/captcha_concern.rb new file mode 100644 index 000000000..5a23e59e3 --- /dev/null +++ b/app/controllers/concerns/captcha_concern.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module CaptchaConcern + extend ActiveSupport::Concern + include Hcaptcha::Adapters::ViewMethods + + CAPTCHA_TIMEOUT = 2.hours.freeze + + included do + helper_method :render_captcha_if_needed + end + + def captcha_available? + ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present? + end + + def captcha_enabled? + captcha_available? && Setting.captcha_enabled + end + + def captcha_recently_passed? + session[:captcha_passed_at].present? && session[:captcha_passed_at] >= CAPTCHA_TIMEOUT.ago + end + + def captcha_required? + captcha_enabled? && !current_user && !(@invite.present? && @invite.valid_for_use? && !@invite.max_uses.nil?) && !captcha_recently_passed? + end + + def clear_captcha! + session.delete(:captcha_passed_at) + end + + def check_captcha! + return true unless captcha_required? + + if verify_hcaptcha + session[:captcha_passed_at] = Time.now.utc + return true + else + if block_given? + message = flash[:hcaptcha_error] + flash.delete(:hcaptcha_error) + yield message + end + return false + end + end + + def extend_csp_for_captcha! + policy = request.content_security_policy + return unless captcha_required? && policy.present? + + %w(script_src frame_src style_src connect_src).each do |directive| + values = policy.send(directive) + values << 'https://hcaptcha.com' unless values.include?('https://hcaptcha.com') || values.include?('https:') + values << 'https://*.hcaptcha.com' unless values.include?('https://*.hcaptcha.com') || values.include?('https:') + policy.send(directive, *values) + end + end + + def render_captcha_if_needed + return unless captcha_required? + + hcaptcha_tags + end +end diff --git a/app/helpers/admin/settings_helper.rb b/app/helpers/admin/settings_helper.rb index baf14ab25..f99a2b8c8 100644 --- a/app/helpers/admin/settings_helper.rb +++ b/app/helpers/admin/settings_helper.rb @@ -8,4 +8,8 @@ module Admin::SettingsHelper link = link_to t('admin.site_uploads.delete'), admin_site_upload_path(upload), data: { method: :delete } safe_join([hint, link], '
'.html_safe) end + + def captcha_available? + ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present? + end end diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index 3433abcdd..64d441fb2 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -1058,3 +1058,7 @@ code { display: none; } } + +.simple_form .h-captcha { + text-align: center; +} diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 3202d1fc2..34f14e312 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -40,6 +40,7 @@ class Form::AdminSettings noindex outgoing_spoilers require_invite_text + captcha_enabled ).freeze BOOLEAN_KEYS = %i( @@ -58,6 +59,7 @@ class Form::AdminSettings trendable_by_default noindex require_invite_text + captcha_enabled ).freeze UPLOAD_KEYS = %i( diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml index e4d614d71..5bb5d08a2 100644 --- a/app/views/about/_registration.html.haml +++ b/app/views/about/_registration.html.haml @@ -21,6 +21,9 @@ .fields-group = f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path), required: true, disabled: closed_registrations? + .fields-group + = render_captcha_if_needed + .actions = f.button :button, sign_up_message, type: :submit, class: 'button button-primary', disabled: closed_registrations? diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index b9daae8f0..49b03a9e3 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -42,7 +42,10 @@ .fields-group = f.input :require_invite_text, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.require_invite_text.title'), hint: t('admin.settings.registrations.require_invite_text.desc_html'), disabled: !approved_registrations? - .fields-group + + - if captcha_available? + .fields-group + = f.input :captcha_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.captcha_enabled.title'), hint: t('admin.settings.captcha_enabled.desc_html') %hr.spacer/ diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml index 6981195ed..5cb558297 100644 --- a/app/views/auth/registrations/new.html.haml +++ b/app/views/auth/registrations/new.html.haml @@ -38,6 +38,9 @@ .fields-group = f.input :agreement, as: :boolean, wrapper: :with_label, label: whitelist_mode? ? t('auth.checkbox_agreement_without_rules_html', terms_path: terms_path) : t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path), required: true + .field-group + = render_captcha_if_needed + .actions = f.button :button, @invite.present? ? t('auth.register') : sign_up_message, type: :submit diff --git a/config/locales-glitch/en.yml b/config/locales-glitch/en.yml index 5cc2625fc..c96f21c92 100644 --- a/config/locales-glitch/en.yml +++ b/config/locales-glitch/en.yml @@ -2,6 +2,9 @@ en: admin: settings: + captcha_enabled: + desc_html: Enable hCaptcha integration, requiring new users to solve a challenge when signing up. Note that this disables app-based registration, and requires third-party scripts from hCaptcha to be embedded in the registration pages. This may have security and privacy concerns. + title: Require new users to go through a CAPTCHA to sign up enable_keybase: desc_html: Allow your users to prove their identity via keybase title: Enable keybase integration diff --git a/config/settings.yml b/config/settings.yml index 094209822..7d192f369 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -77,6 +77,7 @@ defaults: &defaults show_domain_blocks_rationale: 'disabled' outgoing_spoilers: '' require_invite_text: false + captcha_enabled: false development: <<: *defaults -- cgit