From cbe88a1e9c9461c2c20aba427daffc5ba41c78a6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 9 Dec 2024 11:04:46 +0100 Subject: [PATCH] [Glitch] Add terms of service Port 30aa0df88c00cc3597ad87a5b6402de4369e274c to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/api/instance.ts | 11 ++ .../flavours/glitch/api_types/instance.ts | 9 ++ .../flavours/glitch/features/about/index.jsx | 2 +- .../glitch/features/getting_started/index.jsx | 2 +- .../glitch/features/privacy_policy/index.jsx | 65 ----------- .../glitch/features/privacy_policy/index.tsx | 90 +++++++++++++++ .../features/terms_of_service/index.tsx | 95 ++++++++++++++++ .../features/ui/components/compose_panel.jsx | 3 +- .../features/ui/components/link_footer.jsx | 95 ---------------- .../features/ui/components/link_footer.tsx | 105 ++++++++++++++++++ .../flavours/glitch/features/ui/index.jsx | 2 + .../features/ui/util/async-components.js | 4 + .../flavours/glitch/initial_state.js | 3 + .../flavours/glitch/styles/admin.scss | 77 +++++++++++++ 14 files changed, 399 insertions(+), 164 deletions(-) create mode 100644 app/javascript/flavours/glitch/api/instance.ts create mode 100644 app/javascript/flavours/glitch/api_types/instance.ts delete mode 100644 app/javascript/flavours/glitch/features/privacy_policy/index.jsx create mode 100644 app/javascript/flavours/glitch/features/privacy_policy/index.tsx create mode 100644 app/javascript/flavours/glitch/features/terms_of_service/index.tsx delete mode 100644 app/javascript/flavours/glitch/features/ui/components/link_footer.jsx create mode 100644 app/javascript/flavours/glitch/features/ui/components/link_footer.tsx diff --git a/app/javascript/flavours/glitch/api/instance.ts b/app/javascript/flavours/glitch/api/instance.ts new file mode 100644 index 0000000000..16a2ad04cb --- /dev/null +++ b/app/javascript/flavours/glitch/api/instance.ts @@ -0,0 +1,11 @@ +import { apiRequestGet } from 'flavours/glitch/api'; +import type { + ApiTermsOfServiceJSON, + ApiPrivacyPolicyJSON, +} from 'flavours/glitch/api_types/instance'; + +export const apiGetTermsOfService = () => + apiRequestGet('v1/instance/terms_of_service'); + +export const apiGetPrivacyPolicy = () => + apiRequestGet('v1/instance/privacy_policy'); diff --git a/app/javascript/flavours/glitch/api_types/instance.ts b/app/javascript/flavours/glitch/api_types/instance.ts new file mode 100644 index 0000000000..ead9774515 --- /dev/null +++ b/app/javascript/flavours/glitch/api_types/instance.ts @@ -0,0 +1,9 @@ +export interface ApiTermsOfServiceJSON { + updated_at: string; + content: string; +} + +export interface ApiPrivacyPolicyJSON { + updated_at: string; + content: string; +} diff --git a/app/javascript/flavours/glitch/features/about/index.jsx b/app/javascript/flavours/glitch/features/about/index.jsx index c9297e7785..d50b26cfe8 100644 --- a/app/javascript/flavours/glitch/features/about/index.jsx +++ b/app/javascript/flavours/glitch/features/about/index.jsx @@ -18,7 +18,7 @@ import Column from 'flavours/glitch/components/column'; import { Icon } from 'flavours/glitch/components/icon'; import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image'; import { Skeleton } from 'flavours/glitch/components/skeleton'; -import LinkFooter from 'flavours/glitch/features/ui/components/link_footer'; +import { LinkFooter} from 'flavours/glitch/features/ui/components/link_footer'; const messages = defineMessages({ title: { id: 'column.about', defaultMessage: 'About' }, diff --git a/app/javascript/flavours/glitch/features/getting_started/index.jsx b/app/javascript/flavours/glitch/features/getting_started/index.jsx index af6dc313ad..c61edea485 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.jsx +++ b/app/javascript/flavours/glitch/features/getting_started/index.jsx @@ -28,7 +28,7 @@ import { fetchFollowRequests } from 'flavours/glitch/actions/accounts'; import { fetchLists } from 'flavours/glitch/actions/lists'; import { openModal } from 'flavours/glitch/actions/modal'; import Column from 'flavours/glitch/features/ui/components/column'; -import LinkFooter from 'flavours/glitch/features/ui/components/link_footer'; +import { LinkFooter } from 'flavours/glitch/features/ui/components/link_footer'; import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { canManageReports, canViewAdminDashboard } from 'flavours/glitch/permissions'; import { preferencesLink } from 'flavours/glitch/utils/backend_links'; diff --git a/app/javascript/flavours/glitch/features/privacy_policy/index.jsx b/app/javascript/flavours/glitch/features/privacy_policy/index.jsx deleted file mode 100644 index 2b9fc013d8..0000000000 --- a/app/javascript/flavours/glitch/features/privacy_policy/index.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage, FormattedDate, injectIntl, defineMessages } from 'react-intl'; - -import { Helmet } from 'react-helmet'; - -import api from 'flavours/glitch/api'; -import Column from 'flavours/glitch/components/column'; -import { Skeleton } from 'flavours/glitch/components/skeleton'; - -const messages = defineMessages({ - title: { id: 'privacy_policy.title', defaultMessage: 'Privacy Policy' }, -}); - -class PrivacyPolicy extends 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 ( - -
-
-

-

: }} />

-
- -
-
- - - {intl.formatMessage(messages.title)} - - - - ); - } - -} - -export default injectIntl(PrivacyPolicy); diff --git a/app/javascript/flavours/glitch/features/privacy_policy/index.tsx b/app/javascript/flavours/glitch/features/privacy_policy/index.tsx new file mode 100644 index 0000000000..670fda7239 --- /dev/null +++ b/app/javascript/flavours/glitch/features/privacy_policy/index.tsx @@ -0,0 +1,90 @@ +import { useState, useEffect } from 'react'; + +import { + FormattedMessage, + FormattedDate, + useIntl, + defineMessages, +} from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import { apiGetPrivacyPolicy } from 'flavours/glitch/api/instance'; +import type { ApiPrivacyPolicyJSON } from 'flavours/glitch/api_types/instance'; +import { Column } from 'flavours/glitch/components/column'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; + +const messages = defineMessages({ + title: { id: 'privacy_policy.title', defaultMessage: 'Privacy Policy' }, +}); + +const PrivacyPolicy: React.FC<{ + multiColumn: boolean; +}> = ({ multiColumn }) => { + const intl = useIntl(); + const [response, setResponse] = useState(); + const [loading, setLoading] = useState(true); + + useEffect(() => { + apiGetPrivacyPolicy() + .then((data) => { + setResponse(data); + setLoading(false); + return ''; + }) + .catch(() => { + setLoading(false); + }); + }, []); + + return ( + +
+
+

+ +

+

+ + ) : ( + + ), + }} + /> +

+
+ + {response && ( +
+ )} +
+ + + {intl.formatMessage(messages.title)} + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default PrivacyPolicy; diff --git a/app/javascript/flavours/glitch/features/terms_of_service/index.tsx b/app/javascript/flavours/glitch/features/terms_of_service/index.tsx new file mode 100644 index 0000000000..8dad972c44 --- /dev/null +++ b/app/javascript/flavours/glitch/features/terms_of_service/index.tsx @@ -0,0 +1,95 @@ +import { useState, useEffect } from 'react'; + +import { + FormattedMessage, + FormattedDate, + useIntl, + defineMessages, +} from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import { apiGetTermsOfService } from 'flavours/glitch/api/instance'; +import type { ApiTermsOfServiceJSON } from 'flavours/glitch/api_types/instance'; +import { Column } from 'flavours/glitch/components/column'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; +import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; + +const messages = defineMessages({ + title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' }, +}); + +const TermsOfService: React.FC<{ + multiColumn: boolean; +}> = ({ multiColumn }) => { + const intl = useIntl(); + const [response, setResponse] = useState(); + const [loading, setLoading] = useState(true); + + useEffect(() => { + apiGetTermsOfService() + .then((data) => { + setResponse(data); + setLoading(false); + return ''; + }) + .catch(() => { + setLoading(false); + }); + }, []); + + if (!loading && !response) { + return ; + } + + return ( + +
+
+

+ +

+

+ + ) : ( + + ), + }} + /> +

+
+ + {response && ( +
+ )} +
+ + + {intl.formatMessage(messages.title)} + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default TermsOfService; diff --git a/app/javascript/flavours/glitch/features/ui/components/compose_panel.jsx b/app/javascript/flavours/glitch/features/ui/components/compose_panel.jsx index e530b87d26..bb0212859a 100644 --- a/app/javascript/flavours/glitch/features/ui/components/compose_panel.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/compose_panel.jsx @@ -7,10 +7,9 @@ import { mountCompose, unmountCompose } from 'flavours/glitch/actions/compose'; import ServerBanner from 'flavours/glitch/components/server_banner'; import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container'; import SearchContainer from 'flavours/glitch/features/compose/containers/search_container'; +import { LinkFooter } from 'flavours/glitch/features/ui/components/link_footer'; import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; -import LinkFooter from './link_footer'; - class ComposePanel extends PureComponent { static propTypes = { identity: identityContextPropShape, diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx b/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx deleted file mode 100644 index fb07f9e549..0000000000 --- a/app/javascript/flavours/glitch/features/ui/components/link_footer.jsx +++ /dev/null @@ -1,95 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage, injectIntl } from 'react-intl'; - -import { Link } from 'react-router-dom'; - -import { connect } from 'react-redux'; - -import { openModal } from 'flavours/glitch/actions/modal'; -import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; -import { domain, version, source_url, statusPageUrl, profile_directory as profileDirectory } from 'flavours/glitch/initial_state'; -import { PERMISSION_INVITE_USERS } from 'flavours/glitch/permissions'; - -const mapDispatchToProps = (dispatch) => ({ - onLogout () { - dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' })); - - }, -}); - -class LinkFooter extends PureComponent { - static propTypes = { - identity: identityContextPropShape, - multiColumn: PropTypes.bool, - onLogout: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleLogoutClick = e => { - e.preventDefault(); - e.stopPropagation(); - - this.props.onLogout(); - - return false; - }; - - render () { - const { signedIn, permissions } = this.props.identity; - const { multiColumn } = this.props; - - const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS); - const canProfileDirectory = profileDirectory; - - const DividingCircle = {' · '}; - - return ( -
-

- {domain}: - {' '} - - {statusPageUrl && ( - <> - {DividingCircle} - - - )} - {canInvite && ( - <> - {DividingCircle} - - - )} - {canProfileDirectory && ( - <> - {DividingCircle} - - - )} - {DividingCircle} - -

- -

- Mastodon: - {' '} - - {DividingCircle} - - {DividingCircle} - - {DividingCircle} - - {DividingCircle} - v{version} -

-
- ); - } - -} - -export default injectIntl(withIdentity(connect(null, mapDispatchToProps)(LinkFooter))); diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.tsx b/app/javascript/flavours/glitch/features/ui/components/link_footer.tsx new file mode 100644 index 0000000000..0fddce7d09 --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.tsx @@ -0,0 +1,105 @@ +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import { + domain, + version, + source_url, + statusPageUrl, + profile_directory as canProfileDirectory, + termsOfServiceEnabled, +} from 'flavours/glitch/initial_state'; + +const DividingCircle: React.FC = () => {' · '}; + +export const LinkFooter: React.FC<{ + multiColumn: boolean; +}> = ({ multiColumn }) => { + return ( +
+

+ {domain}:{' '} + + + + {statusPageUrl && ( + <> + + + + + + )} + {canProfileDirectory && ( + <> + + + + + + )} + + + + + {termsOfServiceEnabled && ( + <> + + + + + + )} +

+ +

+ Mastodon:{' '} + + + + + + + + + + + + + + + + + v{version} +

+
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx index 9e803bcc96..f66849dacd 100644 --- a/app/javascript/flavours/glitch/features/ui/index.jsx +++ b/app/javascript/flavours/glitch/features/ui/index.jsx @@ -76,6 +76,7 @@ import { Explore, About, PrivacyPolicy, + TermsOfService, } from './util/async-components'; import { ColumnsContextProvider } from './util/columns_context'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; @@ -208,6 +209,7 @@ class SwitchingColumnsArea extends PureComponent { + 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 8530dc95c1..5cd1eac0d1 100644 --- a/app/javascript/flavours/glitch/features/ui/util/async-components.js +++ b/app/javascript/flavours/glitch/features/ui/util/async-components.js @@ -206,6 +206,10 @@ export function PrivacyPolicy () { return import(/*webpackChunkName: "features/glitch/async/privacy_policy" */'../../privacy_policy'); } +export function TermsOfService () { + return import(/*webpackChunkName: "features/glitch/async/terms_of_service" */'../../terms_of_service'); +} + export function NotificationRequests () { return import(/*webpackChunkName: "features/glitch/notifications/requests" */'../../notifications/requests'); } diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js index fd31b54e79..4777f31e29 100644 --- a/app/javascript/flavours/glitch/initial_state.js +++ b/app/javascript/flavours/glitch/initial_state.js @@ -47,6 +47,7 @@ * @property {string} version * @property {string} sso_redirect * @property {string} status_page_url + * @property {boolean} terms_of_service_enabled * @property {boolean} system_emoji_font * @property {string} default_content_type */ @@ -138,6 +139,8 @@ export const criticalUpdatesPending = initialState?.critical_updates_pending; export const statusPageUrl = getMeta('status_page_url'); export const sso_redirect = getMeta('sso_redirect'); +export const termsOfServiceEnabled = getMeta('terms_of_service_enabled'); + // Glitch-soc-specific settings export const maxFeedHashtags = (initialState && initialState.max_feed_hashtags) || 4; export const favouriteModal = getMeta('favourite_modal'); diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index c7134d318b..d23af4914d 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -253,6 +253,10 @@ $content-width: 840px; .time-period { padding: 0 10px; } + + .back-link { + margin-bottom: 0; + } } h2 small { @@ -1961,3 +1965,76 @@ a.sparkline { } } } + +.admin { + &__terms-of-service { + &__container { + background: var(--surface-background-color); + border-radius: 8px; + border: 1px solid var(--background-border-color); + overflow: hidden; + + &__header { + padding: 16px; + font-size: 14px; + line-height: 20px; + color: $secondary-text-color; + display: flex; + align-items: center; + gap: 12px; + } + + &__body { + background: var(--background-color); + padding: 16px; + overflow-y: scroll; + height: 30vh; + } + } + + &__history { + & > li { + border-bottom: 1px solid var(--background-border-color); + + &:last-child { + border-bottom: 0; + } + } + + &__item { + padding: 16px 0; + padding-bottom: 8px; + + h5 { + font-size: 14px; + line-height: 20px; + font-weight: 600; + margin-bottom: 16px; + } + } + } + } +} + +.dot-indicator { + display: inline-flex; + align-items: center; + gap: 8px; + font-weight: 500; + + &__indicator { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background: $dark-text-color; + } + + &.success { + color: $valid-value-color; + + .dot-indicator__indicator { + background-color: $valid-value-color; + } + } +}