[Glitch] Add terms of service

Port 30aa0df88c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
pull/2921/head
Eugen Rochko 2024-12-09 11:04:46 +01:00 committed by Claire
parent 1da3010ffd
commit cbe88a1e9c
14 changed files with 399 additions and 164 deletions

View File

@ -0,0 +1,11 @@
import { apiRequestGet } from 'flavours/glitch/api';
import type {
ApiTermsOfServiceJSON,
ApiPrivacyPolicyJSON,
} from 'flavours/glitch/api_types/instance';
export const apiGetTermsOfService = () =>
apiRequestGet<ApiTermsOfServiceJSON>('v1/instance/terms_of_service');
export const apiGetPrivacyPolicy = () =>
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');

View File

@ -0,0 +1,9 @@
export interface ApiTermsOfServiceJSON {
updated_at: string;
content: string;
}
export interface ApiPrivacyPolicyJSON {
updated_at: string;
content: string;
}

View File

@ -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' },

View File

@ -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';

View File

@ -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 (
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3><FormattedMessage id='privacy_policy.title' defaultMessage='Privacy Policy' /></h3>
<p><FormattedMessage id='privacy_policy.last_updated' defaultMessage='Last updated {date}' values={{ date: isLoading ? <Skeleton width='10ch' /> : <FormattedDate value={lastUpdated} year='numeric' month='short' day='2-digit' /> }} /></p>
</div>
<div
className='privacy-policy__body prose'
dangerouslySetInnerHTML={{ __html: content }}
/>
</div>
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='all' />
</Helmet>
</Column>
);
}
}
export default injectIntl(PrivacyPolicy);

View File

@ -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<ApiPrivacyPolicyJSON>();
const [loading, setLoading] = useState(true);
useEffect(() => {
apiGetPrivacyPolicy()
.then((data) => {
setResponse(data);
setLoading(false);
return '';
})
.catch(() => {
setLoading(false);
});
}, []);
return (
<Column
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.title)}
>
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3>
<FormattedMessage
id='privacy_policy.title'
defaultMessage='Privacy Policy'
/>
</h3>
<p>
<FormattedMessage
id='privacy_policy.last_updated'
defaultMessage='Last updated {date}'
values={{
date: loading ? (
<Skeleton width='10ch' />
) : (
<FormattedDate
value={response?.updated_at}
year='numeric'
month='short'
day='2-digit'
/>
),
}}
/>
</p>
</div>
{response && (
<div
className='privacy-policy__body prose'
dangerouslySetInnerHTML={{ __html: response.content }}
/>
)}
</div>
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='all' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default PrivacyPolicy;

View File

@ -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<ApiTermsOfServiceJSON>();
const [loading, setLoading] = useState(true);
useEffect(() => {
apiGetTermsOfService()
.then((data) => {
setResponse(data);
setLoading(false);
return '';
})
.catch(() => {
setLoading(false);
});
}, []);
if (!loading && !response) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
return (
<Column
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.title)}
>
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3>
<FormattedMessage
id='terms_of_service.title'
defaultMessage='Terms of Service'
/>
</h3>
<p>
<FormattedMessage
id='privacy_policy.last_updated'
defaultMessage='Last updated {date}'
values={{
date: loading ? (
<Skeleton width='10ch' />
) : (
<FormattedDate
value={response?.updated_at}
year='numeric'
month='short'
day='2-digit'
/>
),
}}
/>
</p>
</div>
{response && (
<div
className='privacy-policy__body prose'
dangerouslySetInnerHTML={{ __html: response.content }}
/>
)}
</div>
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='all' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default TermsOfService;

View File

@ -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,

View File

@ -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 = <span aria-hidden>{' · '}</span>;
return (
<div className='link-footer'>
<p>
<strong>{domain}</strong>:
{' '}
<Link to='/about' target={multiColumn ? '_blank' : undefined}><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
{statusPageUrl && (
<>
{DividingCircle}
<a href={statusPageUrl} target='_blank' rel='noopener'><FormattedMessage id='footer.status' defaultMessage='Status' /></a>
</>
)}
{canInvite && (
<>
{DividingCircle}
<a href='/invites' target='_blank'><FormattedMessage id='footer.invite' defaultMessage='Invite people' /></a>
</>
)}
{canProfileDirectory && (
<>
{DividingCircle}
<Link to='/directory'><FormattedMessage id='footer.directory' defaultMessage='Profiles directory' /></Link>
</>
)}
{DividingCircle}
<Link to='/privacy-policy' target={multiColumn ? '_blank' : undefined}><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
</p>
<p>
<strong>Mastodon</strong>:
{' '}
<a href='https://joinmastodon.org' target='_blank'><FormattedMessage id='footer.about' defaultMessage='About' /></a>
{DividingCircle}
<a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='footer.get_app' defaultMessage='Get the app' /></a>
{DividingCircle}
<Link to='/keyboard-shortcuts'><FormattedMessage id='footer.keyboard_shortcuts' defaultMessage='Keyboard shortcuts' /></Link>
{DividingCircle}
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
{DividingCircle}
<span className='version'>v{version}</span>
</p>
</div>
);
}
}
export default injectIntl(withIdentity(connect(null, mapDispatchToProps)(LinkFooter)));

View File

@ -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 = () => <span aria-hidden>{' · '}</span>;
export const LinkFooter: React.FC<{
multiColumn: boolean;
}> = ({ multiColumn }) => {
return (
<div className='link-footer'>
<p>
<strong>{domain}</strong>:{' '}
<Link to='/about' target={multiColumn ? '_blank' : undefined}>
<FormattedMessage id='footer.about' defaultMessage='About' />
</Link>
{statusPageUrl && (
<>
<DividingCircle />
<a href={statusPageUrl} target='_blank' rel='noopener noreferrer'>
<FormattedMessage id='footer.status' defaultMessage='Status' />
</a>
</>
)}
{canProfileDirectory && (
<>
<DividingCircle />
<Link to='/directory'>
<FormattedMessage
id='footer.directory'
defaultMessage='Profiles directory'
/>
</Link>
</>
)}
<DividingCircle />
<Link
to='/privacy-policy'
target={multiColumn ? '_blank' : undefined}
rel='privacy-policy'
>
<FormattedMessage
id='footer.privacy_policy'
defaultMessage='Privacy policy'
/>
</Link>
{termsOfServiceEnabled && (
<>
<DividingCircle />
<Link
to='/terms-of-service'
target={multiColumn ? '_blank' : undefined}
rel='terms-of-service'
>
<FormattedMessage
id='footer.terms_of_service'
defaultMessage='Terms of service'
/>
</Link>
</>
)}
</p>
<p>
<strong>Mastodon</strong>:{' '}
<a href='https://joinmastodon.org' target='_blank' rel='noreferrer'>
<FormattedMessage id='footer.about' defaultMessage='About' />
</a>
<DividingCircle />
<a
href='https://joinmastodon.org/apps'
target='_blank'
rel='noreferrer'
>
<FormattedMessage id='footer.get_app' defaultMessage='Get the app' />
</a>
<DividingCircle />
<Link to='/keyboard-shortcuts'>
<FormattedMessage
id='footer.keyboard_shortcuts'
defaultMessage='Keyboard shortcuts'
/>
</Link>
<DividingCircle />
<a href={source_url} rel='noopener noreferrer' target='_blank'>
<FormattedMessage
id='footer.source_code'
defaultMessage='View source code'
/>
</a>
<DividingCircle />
<span className='version'>v{version}</span>
</p>
</div>
);
};

View File

@ -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 {
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
<WrappedRoute path='/about' component={About} content={children} />
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
<WrappedRoute path='/terms-of-service' component={TermsOfService} content={children} />
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
<Redirect from='/timelines/public' to='/public' exact />

View File

@ -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');
}

View File

@ -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');

View File

@ -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;
}
}
}