Add terms of service (#33055)

This commit is contained in:
Eugen Rochko 2024-12-09 11:04:46 +01:00 committed by GitHub
parent 7a2a345c08
commit 30aa0df88c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
129 changed files with 1456 additions and 238 deletions

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class Admin::TermsOfService::DistributionsController < Admin::BaseController
before_action :set_terms_of_service
def create
authorize @terms_of_service, :distribute?
@terms_of_service.touch(:notification_sent_at)
Admin::DistributeTermsOfServiceNotificationWorker.perform_async(@terms_of_service.id)
redirect_to admin_terms_of_service_index_path
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.find(params[:terms_of_service_id])
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
class Admin::TermsOfService::DraftsController < Admin::BaseController
before_action :set_terms_of_service
def show
authorize :terms_of_service, :create?
end
def update
authorize @terms_of_service, :update?
@terms_of_service.published_at = Time.now.utc if params[:action_type] == 'publish'
if @terms_of_service.update(resource_params)
log_action(:publish, @terms_of_service) if @terms_of_service.published?
redirect_to @terms_of_service.published? ? admin_terms_of_service_index_path : admin_terms_of_service_draft_path
else
render :show
end
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text)
end
def current_terms_of_service
TermsOfService.live.first
end
def resource_params
params.require(:terms_of_service).permit(:text, :changelog)
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
class Admin::TermsOfService::GeneratesController < Admin::BaseController
before_action :set_instance_presenter
def show
authorize :terms_of_service, :create?
@generator = TermsOfService::Generator.new(
domain: @instance_presenter.domain,
admin_email: @instance_presenter.contact.email
)
end
def create
authorize :terms_of_service, :create?
@generator = TermsOfService::Generator.new(resource_params)
if @generator.valid?
TermsOfService.create!(text: @generator.render)
redirect_to admin_terms_of_service_draft_path
else
render :show
end
end
private
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def resource_params
params.require(:terms_of_service_generator).permit(*TermsOfService::Generator::VARIABLES)
end
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class Admin::TermsOfService::HistoriesController < Admin::BaseController
def show
authorize :terms_of_service, :index?
@terms_of_service = TermsOfService.published.all
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Admin::TermsOfService::PreviewsController < Admin::BaseController
before_action :set_terms_of_service
def show
authorize @terms_of_service, :distribute?
@user_count = @terms_of_service.scope_for_notification.count
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.find(params[:terms_of_service_id])
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Admin::TermsOfService::TestsController < Admin::BaseController
before_action :set_terms_of_service
def create
authorize @terms_of_service, :distribute?
UserMailer.terms_of_service_changed(current_user, @terms_of_service).deliver_later!
redirect_to admin_terms_of_service_preview_path(@terms_of_service)
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.find(params[:terms_of_service_id])
end
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class Admin::TermsOfServiceController < Admin::BaseController
def index
authorize :terms_of_service, :index?
@terms_of_service = TermsOfService.live.first
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseController
before_action :set_terms_of_service
def show
cache_even_if_authenticated!
render json: @terms_of_service, serializer: REST::PrivacyPolicySerializer
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.live.first!
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class TermsOfServiceController < ApplicationController
include WebAppControllerConcern
skip_before_action :require_functional!
def show
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
end
end

View File

@ -64,6 +64,10 @@ module FormattingHelper
end
end
def markdown(text)
Redcarpet::Markdown.new(Redcarpet::Render::HTML, escape_html: true, no_images: true).render(text).html_safe # rubocop:disable Rails/OutputSafety
end
private
def wrapped_status_content_format(status)

View File

@ -0,0 +1,11 @@
import { apiRequestGet } from 'mastodon/api';
import type {
ApiTermsOfServiceJSON,
ApiPrivacyPolicyJSON,
} from 'mastodon/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 'mastodon/components/column';
import { Icon } from 'mastodon/components/icon';
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
import { Skeleton } from 'mastodon/components/skeleton';
import LinkFooter from 'mastodon/features/ui/components/link_footer';
import { LinkFooter} from 'mastodon/features/ui/components/link_footer';
const messages = defineMessages({
title: { id: 'column.about', defaultMessage: 'About' },

View File

@ -25,7 +25,7 @@ import StarIcon from '@/material-icons/400-24px/star.svg?react';
import { fetchFollowRequests } from 'mastodon/actions/accounts';
import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
import LinkFooter from 'mastodon/features/ui/components/link_footer';
import { LinkFooter } from 'mastodon/features/ui/components/link_footer';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { canManageReports, canViewAdminDashboard } from 'mastodon/permissions';

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 'mastodon/api';
import Column from 'mastodon/components/column';
import { Skeleton } from 'mastodon/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 'mastodon/api/instance';
import type { ApiPrivacyPolicyJSON } from 'mastodon/api_types/instance';
import { Column } from 'mastodon/components/column';
import { Skeleton } from 'mastodon/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 'mastodon/api/instance';
import type { ApiTermsOfServiceJSON } from 'mastodon/api_types/instance';
import { Column } from 'mastodon/components/column';
import { Skeleton } from 'mastodon/components/skeleton';
import BundleColumnError from 'mastodon/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 { changeComposing, mountCompose, unmountCompose } from 'mastodon/actions/
import ServerBanner from 'mastodon/components/server_banner';
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
import SearchContainer from 'mastodon/features/compose/containers/search_container';
import { LinkFooter } from 'mastodon/features/ui/components/link_footer';
import { identityContextPropShape, withIdentity } from 'mastodon/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 'mastodon/actions/modal';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { domain, version, source_url, statusPageUrl, profile_directory as profileDirectory } from 'mastodon/initial_state';
import { PERMISSION_INVITE_USERS } from 'mastodon/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 'mastodon/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

@ -71,6 +71,7 @@ import {
Explore,
About,
PrivacyPolicy,
TermsOfService,
} from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
@ -198,6 +199,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

@ -198,6 +198,10 @@ export function PrivacyPolicy () {
return import(/*webpackChunkName: "features/privacy_policy" */'../../privacy_policy');
}
export function TermsOfService () {
return import(/*webpackChunkName: "features/terms_of_service" */'../../terms_of_service');
}
export function NotificationRequests () {
return import(/*webpackChunkName: "features/notifications/requests" */'../../notifications/requests');
}

View File

@ -43,6 +43,8 @@
* @property {boolean=} use_pending_items
* @property {string} version
* @property {string} sso_redirect
* @property {string} status_page_url
* @property {boolean} terms_of_service_enabled
*/
/**
@ -115,10 +117,9 @@ export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version');
export const languages = initialState?.languages;
export const criticalUpdatesPending = initialState?.critical_updates_pending;
// @ts-expect-error
export const statusPageUrl = getMeta('status_page_url');
export const sso_redirect = getMeta('sso_redirect');
export const termsOfServiceEnabled = getMeta('terms_of_service_enabled');
/**
* @returns {string | undefined}
*/

View File

@ -359,11 +359,11 @@
"footer.about": "About",
"footer.directory": "Profiles directory",
"footer.get_app": "Get the app",
"footer.invite": "Invite people",
"footer.keyboard_shortcuts": "Keyboard shortcuts",
"footer.privacy_policy": "Privacy policy",
"footer.source_code": "View source code",
"footer.status": "Status",
"footer.terms_of_service": "Terms of service",
"generic.saved": "Saved",
"getting_started.heading": "Getting started",
"hashtag.admin_moderation": "Open moderation interface for #{name}",
@ -857,6 +857,7 @@
"subscribed_languages.target": "Change subscribed languages for {target}",
"tabs_bar.home": "Home",
"tabs_bar.notifications": "Notifications",
"terms_of_service.title": "Terms of Service",
"time_remaining.days": "{number, plural, one {# day} other {# days}} left",
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",

View File

@ -173,7 +173,9 @@ table + p {
}
.email-prose {
p {
p,
ul,
ol {
color: #17063b;
font-size: 14px;
line-height: 20px;

View File

@ -253,6 +253,10 @@ $content-width: 840px;
.time-period {
padding: 0 10px;
}
.back-link {
margin-bottom: 0;
}
}
h2 small {
@ -1940,3 +1944,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;
}
}
}

View File

@ -209,6 +209,16 @@ class UserMailer < Devise::Mailer
end
end
def terms_of_service_changed(user, terms_of_service)
@resource = user
@terms_of_service = terms_of_service
@markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, escape_html: true, no_images: true)
I18n.with_locale(locale) do
mail subject: default_i18n_subject
end
end
private
def default_devise_subject

View File

@ -57,6 +57,7 @@ class Admin::ActionLogFilter
enable_relay: { target_type: 'Relay', action: 'enable' }.freeze,
memorialize_account: { target_type: 'Account', action: 'memorialize' }.freeze,
promote_user: { target_type: 'User', action: 'promote' }.freeze,
publish_terms_of_service: { target_type: 'TermsOfService', action: 'publish' }.freeze,
remove_avatar_user: { target_type: 'User', action: 'remove_avatar' }.freeze,
reopen_report: { target_type: 'Report', action: 'reopen' }.freeze,
resend_user: { target_type: 'User', action: 'resend' }.freeze,

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: terms_of_services
#
# id :bigint(8) not null, primary key
# changelog :text default(""), not null
# notification_sent_at :datetime
# published_at :datetime
# text :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class TermsOfService < ApplicationRecord
scope :published, -> { where.not(published_at: nil).order(published_at: :desc) }
scope :live, -> { published.limit(1) }
scope :draft, -> { where(published_at: nil).order(id: :desc).limit(1) }
validates :text, presence: true
validates :changelog, presence: true, if: -> { published? }
def published?
published_at.present?
end
def notification_sent?
notification_sent_at.present?
end
def scope_for_notification
User.confirmed.joins(:account).merge(Account.without_suspended).where(created_at: (..published_at))
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class TermsOfService::Generator
include ActiveModel::Model
TEMPLATE = Rails.root.join('config', 'templates', 'terms-of-service.md').read
VARIABLES = %i(
admin_email
arbitration_address
arbitration_website
dmca_address
dmca_email
domain
jurisdiction
).freeze
attr_accessor(*VARIABLES)
validates(*VARIABLES, presence: true)
def render
format(TEMPLATE, VARIABLES.index_with { |key| public_send(key) })
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
class TermsOfServicePolicy < ApplicationPolicy
def index?
role.can?(:manage_settings)
end
def create?
role.can?(:manage_settings)
end
def distribute?
record.published? && !record.notification_sent? && role.can?(:manage_settings)
end
def update?
!record.published? && role.can?(:manage_settings)
end
def destroy?
!record.published? && role.can?(:manage_settings)
end
end

View File

@ -109,6 +109,7 @@ class InitialStateSerializer < ActiveModel::Serializer
trends_as_landing_page: Setting.trends_as_landing_page,
trends_enabled: Setting.trends,
version: instance_presenter.version,
terms_of_service_enabled: TermsOfService.live.exists?,
}
end

View File

@ -0,0 +1,6 @@
.content__heading__tabs
= render_navigation renderer: :links do |primary|
:ruby
primary.item :current, safe_join([material_symbol('description'), t('admin.terms_of_service.current')]), admin_terms_of_service_index_path
primary.item :draft, safe_join([material_symbol('description'), t('admin.terms_of_service.draft')]), admin_terms_of_service_draft_path
primary.item :previous, safe_join([material_symbol('history'), t('admin.terms_of_service.history')]), admin_terms_of_service_history_path

View File

@ -0,0 +1,19 @@
- content_for :page_title do
= t('admin.terms_of_service.title')
- content_for :heading do
%h2= t('admin.terms_of_service.title')
= render partial: 'admin/terms_of_service/links'
= simple_form_for @terms_of_service, url: admin_terms_of_service_draft_path, method: :put do |form|
= render 'shared/error_messages', object: @terms_of_service
.fields-group
= form.input :text, wrapper: :with_block_label, input_html: { rows: 8 }
.fields-group
= form.input :changelog, wrapper: :with_block_label, input_html: { rows: 8 }
.actions
= form.button :button, t('admin.terms_of_service.save_draft'), type: :submit, name: :action_type, value: :save_draft, class: 'button button-secondary'
= form.button :button, t('admin.terms_of_service.publish'), type: :submit, name: :action_type, value: :publish

View File

@ -0,0 +1,41 @@
- content_for :page_title do
= t('admin.terms_of_service.generates.title')
- content_for :heading_actions do
.back-link
= link_to admin_terms_of_service_index_path do
= material_symbol 'chevron_left'
= t('admin.terms_of_service.back')
%p.lead= t('admin.terms_of_service.generates.explanation_html')
%p.lead= t('admin.terms_of_service.generates.chance_to_review_html')
%hr.spacer/
= simple_form_for @generator, url: admin_terms_of_service_generate_path, method: :post do |form|
= render 'shared/error_messages', object: @generator
.fields-group
= form.input :domain, wrapper: :with_label
.fields-group
= form.input :jurisdiction, wrapper: :with_label
.fields-group
= form.input :admin_email, wrapper: :with_label
.fields-group
= form.input :dmca_email, wrapper: :with_label
.fields-group
= form.input :dmca_address, wrapper: :with_label
.fields-group
= form.input :arbitration_address, wrapper: :with_label
.fields-group
= form.input :arbitration_website, wrapper: :with_label
.actions
= form.button :button, t('admin.terms_of_service.generates.action'), type: :submit

View File

@ -0,0 +1,16 @@
- content_for :page_title do
= t('admin.terms_of_service.history')
- content_for :heading do
%h2= t('admin.terms_of_service.title')
= render partial: 'admin/terms_of_service/links'
- if @terms_of_service.empty?
%p= t('admin.terms_of_service.no_history')
- else
%ol.admin__terms-of-service__history
- @terms_of_service.each do |terms_of_service|
%li
.admin__terms-of-service__history__item
%h5= l(terms_of_service.published_at)
.prose= markdown(terms_of_service.changelog)

View File

@ -0,0 +1,39 @@
- content_for :page_title do
= t('admin.terms_of_service.title')
- content_for :heading do
%h2= t('admin.terms_of_service.title')
= render partial: 'links'
- if @terms_of_service.present?
.admin__terms-of-service__container
.admin__terms-of-service__container__header
.dot-indicator.success
.dot-indicator__indicator
%span= t('admin.terms_of_service.live')
·
%span
= t('admin.terms_of_service.published_on_html', date: tag.time(l(@terms_of_service.published_at.to_date), class: 'formatted', date: @terms_of_service.published_at.to_date.iso8601))
·
- if @terms_of_service.notification_sent?
%span
= t('admin.terms_of_service.notified_on_html', date: tag.time(l(@terms_of_service.notification_sent_at.to_date), class: 'formatted', date: @terms_of_service.notification_sent_at.to_date.iso8601))
- else
= link_to t('admin.terms_of_service.notify_users'), admin_terms_of_service_preview_path(@terms_of_service), class: 'link-button'
.admin__terms-of-service__container__body
.prose
= markdown(@terms_of_service.text)
%hr.spacer/
%h3= t('admin.terms_of_service.changelog')
.prose
= markdown(@terms_of_service.changelog)
- else
%p.lead= t('admin.terms_of_service.no_terms_of_service_html')
.content__heading__actions
= link_to t('admin.terms_of_service.create'), admin_terms_of_service_draft_path, class: 'button'
= link_to t('admin.terms_of_service.generate'), admin_terms_of_service_generate_path, class: 'button button-secondary'

View File

@ -0,0 +1,20 @@
- content_for :page_title do
= t('admin.terms_of_service.preview.title')
- content_for :heading_actions do
.back-link
= link_to admin_terms_of_service_index_path do
= material_symbol 'chevron_left'
= t('admin.terms_of_service.back')
%p.lead
= t('admin.terms_of_service.preview.explanation_html', count: @user_count, display_count: number_with_delimiter(@user_count), date: l(@terms_of_service.published_at.to_date))
.prose
= markdown(@terms_of_service.changelog)
%hr.spacer/
.content__heading__actions
= link_to t('admin.terms_of_service.preview.send_preview', email: current_user.email), admin_terms_of_service_test_path(@terms_of_service), method: :post, class: 'button button-secondary'
= link_to t('admin.terms_of_service.preview.send_to_all', count: @user_count, display_count: number_with_delimiter(@user_count)), admin_terms_of_service_distribution_path(@terms_of_service), method: :post, class: 'button', data: { confirm: t('admin.reports.are_you_sure') }

View File

@ -72,7 +72,7 @@
.fields-group
= f.input :agreement,
as: :boolean,
label: t('auth.privacy_policy_agreement_html', rules_path: about_more_path, privacy_policy_path: privacy_policy_path),
label: t('auth.user_agreement_html', privacy_policy_path: privacy_policy_path, terms_of_service_path: terms_of_service_path),
required: false,
wrapper: :with_label

View File

@ -0,0 +1,6 @@
- content_for :page_title, t('terms_of_service.title')
- content_for :header_tags do
= render partial: 'shared/og'
= render 'shared/web_app'

View File

@ -0,0 +1,17 @@
= content_for :heading do
= render 'application/mailer/heading',
image_url: frontend_asset_url('images/mailer-new/heading/user.png'),
subtitle: t('user_mailer.terms_of_service_changed.subtitle', domain: site_hostname),
title: t('user_mailer.terms_of_service_changed.title')
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
%tr
%td.email-body-padding-td
%table.email-inner-card-table{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
%tr
%td.email-inner-card-td.email-prose
%p= t('user_mailer.terms_of_service_changed.description_html', path: terms_of_service_url, domain: site_hostname)
%p
%strong= t('user_mailer.terms_of_service_changed.changelog')
= markdown(@terms_of_service.changelog)
%p= t('user_mailer.terms_of_service_changed.agreement', domain: site_hostname)
%p= t('user_mailer.terms_of_service_changed.sign_off', domain: site_hostname)

View File

@ -0,0 +1,14 @@
<%= t('user_mailer.terms_of_service_changed.title') %>
===
<%= t('user_mailer.terms_of_service_changed.description', domain: site_hostname) %>
=> <%= terms_of_service_url %>
<%= t('user_mailer.terms_of_service_changed.changelog') %>
<%= @terms_of_service.changelog %>
<%= t('user_mailer.terms_of_service_changed.agreement', domain: site_hostname) %>
<%= t('user_mailer.terms_of_service_changed.sign_off', domain: site_hostname) %>

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class Admin::DistributeTermsOfServiceNotificationWorker
include Sidekiq::Worker
def perform(terms_of_service_id)
terms_of_service = TermsOfService.find(terms_of_service_id)
terms_of_service.scope_for_notification.find_each do |user|
UserMailer.terms_of_service_changed(user, terms_of_service).deliver_later!
end
rescue ActiveRecord::RecordNotFound
true
end
end

View File

@ -919,7 +919,6 @@ an:
migrate_account: Mudar-se a unatra cuenta
migrate_account_html: Si deseyas reendrezar esta cuenta a unatra distinta, puetz <a href="%{path}">configurar-lo aquí</a>.
or_log_in_with: U inicia sesión con
privacy_policy_agreement_html: He leyiu y accepto la <a href="%{privacy_policy_path}" target="_blank">politica de privacidat</a>
providers:
cas: CAS
saml: SAML

View File

@ -1118,7 +1118,6 @@ ar:
migrate_account: الانتقال إلى حساب مختلف
migrate_account_html: إن كنت ترغب في تحويل هذا الحساب نحو حساب آخَر، يُمكِنُك <a href="%{path}">إعداده هنا</a>.
or_log_in_with: أو قم بتسجيل الدخول بواسطة
privacy_policy_agreement_html: لقد قرأتُ وأوافق على سياسة الخصوصية <a href="%{privacy_policy_path}" target="_blank"></a>
progress:
confirm: تأكيد عنوان البريد الإلكتروني
details: تفاصيلك

View File

@ -459,7 +459,6 @@ ast:
logout: Zarrar la sesión
migrate_account: Cambéu de cuenta
migrate_account_html: Si quies redirixir esta cuenta a otra diferente, pues <a href="%{path}">configurar esta opción equí</a>.
privacy_policy_agreement_html: Lleí y acepto la <a href="%{privacy_policy_path}" target="_blank">política de privacidá</a>
providers:
cas: CAS
saml: SAML

View File

@ -1134,7 +1134,6 @@ be:
migrate_account: Пераехаць на іншы ўліковы запіс
migrate_account_html: Калі вы хочаце перанакіраваць гэты ўліковы запіс на іншы, то можаце <a href="%{path}">наладзіць яго тут</a>.
or_log_in_with: Або ўвайсці з дапамогай
privacy_policy_agreement_html: Я азнаёміўся і пагаджаюся з <a href="%{privacy_policy_path}" target="_blank">палітыкай канфідэнцыйнасці</a>
progress:
confirm: Пацвердзіць email
details: Вашы дадзеныя

View File

@ -1121,7 +1121,6 @@ bg:
migrate_account: Преместване в различен акаунт
migrate_account_html: Ако желаете да пренасочите този акаунт към друг, можете да <a href="%{path}">настроите това тук</a>.
or_log_in_with: Или влизане с помощта на
privacy_policy_agreement_html: Прочетох и има съгласието ми за <a href="%{privacy_policy_path}" target="_blank">политиката за поверителност</a>
progress:
details: Вашите подробности
review: Нашият преглед

View File

@ -1132,7 +1132,6 @@ ca:
migrate_account: Mou a un compte diferent
migrate_account_html: Si vols redirigir aquest compte a un altre diferent, el pots <a href="%{path}">configurar aquí</a>.
or_log_in_with: O inicia sessió amb
privacy_policy_agreement_html: He llegit i estic d'acord amb la <a href="%{privacy_policy_path}" target="_blank">política de privacitat</a>
progress:
confirm: Confirmar email
details: Els teus detalls

View File

@ -1099,7 +1099,6 @@ cs:
migrate_account: Přesunout se na jiný účet
migrate_account_html: Zde můžete <a href="%{path}">nastavit přesměrování tohoto účtu na jiný</a>.
or_log_in_with: Nebo se přihlaste pomocí
privacy_policy_agreement_html: Četl jsem a souhlasím se zásadami <a href="%{privacy_policy_path}" target="_blank">ochrany osobních údajů</a>
progress:
details: Vaše údaje
review: Naše hodnocení

View File

@ -1204,7 +1204,6 @@ cy:
migrate_account: Symud i gyfrif gwahanol
migrate_account_html: Os hoffech chi ailgyfeirio'r cyfrif hwn at un gwahanol, mae modd <a href="%{path}">ei ffurfweddu yma</a>.
or_log_in_with: Neu mewngofnodwch gyda
privacy_policy_agreement_html: Rwyf wedi darllen ac yn cytuno i'r <a href="%{privacy_policy_path}" target="_blank">polisi preifatrwydd</a>
progress:
confirm: Cadarnhau'r e-bost
details: Eich manylion

View File

@ -1132,7 +1132,6 @@ da:
migrate_account: Flyt til en anden konto
migrate_account_html: Ønsker du at omdirigere denne konto til en anden, kan du <a href="%{path}">opsætte dette hér</a>.
or_log_in_with: Eller log ind med
privacy_policy_agreement_html: Jeg accepterer <a href="%{privacy_policy_path}" target="_blank">privatlivspolitikken</a>
progress:
confirm: Bekræft e-mail
details: Dine detaljer

View File

@ -1132,7 +1132,6 @@ de:
migrate_account: Zu einem anderen Konto umziehen
migrate_account_html: Wenn du dieses Konto auf ein anderes weiterleiten möchtest, kannst du es <a href="%{path}">hier konfigurieren</a>.
or_log_in_with: Oder anmelden mit
privacy_policy_agreement_html: Ich habe die <a href="%{privacy_policy_path}" target="_blank">Datenschutzerklärung</a> gelesen und stimme ihr zu
progress:
confirm: E-Mail bestätigen
details: Deine Daten

View File

@ -1132,7 +1132,6 @@ el:
migrate_account: Μεταφορά σε διαφορετικό λογαριασμό
migrate_account_html: Αν θέλεις να ανακατευθύνεις αυτό τον λογαριασμό σε έναν διαφορετικό, μπορείς να το <a href="%{path}">διαμορφώσεις εδώ</a>.
or_log_in_with: Ή συνδέσου με
privacy_policy_agreement_html: Έχω διαβάσει και συμφωνώ με την <a href="%{privacy_policy_path}" target="_blank">πολιτική απορρήτου</a>
progress:
confirm: Επιβεβαίωση email
details: Τα στοιχεία σας

View File

@ -1132,7 +1132,6 @@ en-GB:
migrate_account: Move to a different account
migrate_account_html: If you wish to redirect this account to a different one, you can <a href="%{path}">configure it here</a>.
or_log_in_with: Or log in with
privacy_policy_agreement_html: I have read and agree to the <a href="%{privacy_policy_path}" target="_blank">privacy policy</a>
progress:
confirm: Confirm email
details: Your details

View File

@ -214,6 +214,7 @@ en:
enable_user: Enable User
memorialize_account: Memorialize Account
promote_user: Promote User
publish_terms_of_service: Publish Terms of Service
reject_appeal: Reject Appeal
reject_user: Reject User
remove_avatar_user: Remove Avatar
@ -278,6 +279,7 @@ en:
enable_user_html: "%{name} enabled login for user %{target}"
memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page"
promote_user_html: "%{name} promoted user %{target}"
publish_terms_of_service_html: "%{name} published updates to the terms of service"
reject_appeal_html: "%{name} rejected moderation decision appeal from %{target}"
reject_user_html: "%{name} rejected sign-up from %{target}"
remove_avatar_user_html: "%{name} removed %{target}'s avatar"
@ -925,6 +927,35 @@ en:
search: Search
title: Hashtags
updated_msg: Hashtag settings updated successfully
terms_of_service:
back: Back to terms of service
changelog: What's changed
create: Use your own
current: Current
draft: Draft
generate: Use template
generates:
action: Generate
chance_to_review_html: "<strong>The generated terms of service will not be published automatically.</strong> You will have a chance to review the results. Please fill in the necessary details to proceed."
explanation_html: The terms of service template provided is for informational purposes only, and should not be construed as legal advice on any subject matter. Please consult with your own legal counsel on your situation and specific legal questions you have.
title: Terms of Service Setup
history: History
live: Live
no_history: There are no recorded changes of the terms of service yet.
no_terms_of_service_html: You don't currently have any terms of service configured. Terms of service are meant to provide clarity and protect you from potential liabilities in disputes with your users.
notified_on_html: Users notified on %{date}
notify_users: Notify users
preview:
explanation_html: 'The email will be sent to <strong>%{display_count} users</strong> who have signed up before %{date}. The following text will be included in the e-mail:'
send_preview: Send preview to %{email}
send_to_all:
one: Send %{display_count} email
other: Send %{display_count} emails
title: Preview terms of service notification
publish: Publish
published_on_html: Published on %{date}
save_draft: Save draft
title: Terms of Service
title: Administration
trends:
allow: Allow
@ -1132,7 +1163,6 @@ en:
migrate_account: Move to a different account
migrate_account_html: If you wish to redirect this account to a different one, you can <a href="%{path}">configure it here</a>.
or_log_in_with: Or log in with
privacy_policy_agreement_html: I have read and agree to the <a href="%{privacy_policy_path}" target="_blank">privacy policy</a>
progress:
confirm: Confirm email
details: Your details
@ -1178,6 +1208,7 @@ en:
view_strikes: View past strikes against your account
too_fast: Form submitted too fast, try again.
use_security_key: Use security key
user_agreement_html: I have read and agree to the <a href="%{terms_of_service_path}" target="_blank">terms of service</a> and <a href="%{privacy_policy_path}" target="_blank">privacy policy</a>
author_attribution:
example_title: Sample text
hint_html: Are you writing news or blog articles outside of Mastodon? Control how you get credited when they are shared on Mastodon.
@ -1840,6 +1871,8 @@ en:
too_late: It is too late to appeal this strike
tags:
does_not_match_previous_name: does not match the previous name
terms_of_service:
title: Terms of Service
themes:
contrast: Mastodon (High contrast)
default: Mastodon (Dark)
@ -1900,6 +1933,15 @@ en:
further_actions_html: If this wasn't you, we recommend that you %{action} immediately and enable two-factor authentication to keep your account secure.
subject: Your account has been accessed from a new IP address
title: A new sign-in
terms_of_service_changed:
agreement: By continuing to use %{domain}, you are agreeing to these terms. If you disagree with the updated terms, you may terminate your agreement with %{domain} at any time by deleting your account.
changelog: 'At a glance, here is what this update means for you:'
description: 'You are receiving this e-mail because we''re making some changes to our terms of service at %{domain}. We encourage you to review the updated terms in full here:'
description_html: You are receiving this e-mail because we're making some changes to our terms of service at %{domain}. We encourage you to review the <a href="%{path}" target="_blank">updated terms in full here</a>.
sign_off: The %{domain} team
subject: Updates to our terms of service
subtitle: The terms of service of %{domain} are changing
title: Important update
warning:
appeal: Submit an appeal
appeal_description: If you believe this is an error, you can submit an appeal to the staff of %{instance}.

View File

@ -1132,7 +1132,6 @@ eo:
migrate_account: Movi al alia konto
migrate_account_html: Se vi deziras alidirekti ĉi tiun konton al alia, vi povas <a href="%{path}">agordi ĝin ĉi tie</a>.
or_log_in_with: Aŭ saluti per
privacy_policy_agreement_html: Mi legis kaj konsentis pri <a href="%{privacy_policy_path}" target="_blank">privatpolitiko</a>
progress:
confirm: Konfirmi retadreson
details: Viaj detaloj

View File

@ -1132,7 +1132,6 @@ es-AR:
migrate_account: Mudarse a otra cuenta
migrate_account_html: Si querés redireccionar esta cuenta a otra distinta, podés <a href="%{path}">configurar eso acá</a>.
or_log_in_with: O iniciar sesión con
privacy_policy_agreement_html: Leí y acepto la <a href="%{privacy_policy_path}" target="_blank">política de privacidad</a>
progress:
confirm: Confirmar correo electrónico
details: Tus detalles

View File

@ -1132,7 +1132,6 @@ es-MX:
migrate_account: Mudarse a otra cuenta
migrate_account_html: Si deseas redireccionar esta cuenta a otra distinta, puedes <a href="%{path}">configurarlo aquí</a>.
or_log_in_with: O inicia sesión con
privacy_policy_agreement_html: He leído y acepto la <a href="%{privacy_policy_path}" target="_blank">política de privacidad</a>
progress:
confirm: Confirmar dirección de correo
details: Tus detalles

View File

@ -1132,7 +1132,6 @@ es:
migrate_account: Mudarse a otra cuenta
migrate_account_html: Si deseas redireccionar esta cuenta a otra distinta, puedes <a href="%{path}">configurarlo aquí</a>.
or_log_in_with: O inicia sesión con
privacy_policy_agreement_html: He leído y acepto la <a href="%{privacy_policy_path}" target="_blank">política de privacidad</a>
progress:
confirm: Confirmar dirección de correo
details: Tus detalles

View File

@ -1117,7 +1117,6 @@ et:
migrate_account: Teisele kontole ära kolimine
migrate_account_html: Kui soovid konto siit ära kolida, <a href="%{path}">saad seda teha siin</a>.
or_log_in_with: Või logi sisse koos
privacy_policy_agreement_html: Olen tutvunud <a href="%{privacy_policy_path}" target="_blank">isikuandmete kaitse põhimõtetega</a> ja nõustun nendega
progress:
confirm: E-posti kinnitamine
details: Sinu üksikasjad

View File

@ -1041,7 +1041,6 @@ eu:
migrate_account: Migratu beste kontu batera
migrate_account_html: Kontu hau beste batera birbideratu nahi baduzu, <a href="%{path}">hemen konfiguratu</a> dezakezu.
or_log_in_with: Edo hasi saioa honekin
privacy_policy_agreement_html: <a href="%{privacy_policy_path}" target="_blank">Pribatutasun politika</a> irakurri dut eta ados nago
progress:
details: Zure xehetasunak
review: Gure berrikuspena

View File

@ -978,7 +978,6 @@ fa:
migrate_account: نقل مکان به یک حساب دیگر
migrate_account_html: اگر می‌خواهید این حساب را به حساب دیگری منتقل کنید، <a href="%{path}">این‌جا را کلیک کنید</a>.
or_log_in_with: یا ورود به وسیلهٔ
privacy_policy_agreement_html: <a href="%{privacy_policy_path}" target="_blank">سیاست محرمانگی</a> را خوانده و پذیرفته‌ام
progress:
confirm: تأیید رایانامه
details: جزئیات شما

View File

@ -1132,7 +1132,6 @@ fi:
migrate_account: Muuta toiseen tiliin
migrate_account_html: Jos haluat ohjata tämän tilin toiseen, voit <a href="%{path}">asettaa toisen tilin tästä</a>.
or_log_in_with: Tai käytä kirjautumiseen
privacy_policy_agreement_html: Olen lukenut ja hyväksyn <a href="%{privacy_policy_path}" target="_blank">tietosuojakäytännön</a>
progress:
confirm: Vahvista sähköpostiosoite
details: Omat tietosi

View File

@ -1132,7 +1132,6 @@ fo:
migrate_account: Flyt til eina aðra kontu
migrate_account_html: Ynskir tú at víðaribeina hesa kontuna til eina aðra, so kanst tú <a href="%{path}">seta tað upp her</a>.
or_log_in_with: Ella innrita við
privacy_policy_agreement_html: Eg havi lisið og taki undir við <a href="%{privacy_policy_path}" target="_blank">privatlívspolitikkinum</a>
progress:
confirm: Vátta teldupost
details: Tínir smálutir

View File

@ -1135,7 +1135,6 @@ fr-CA:
migrate_account: Déménager vers un compte différent
migrate_account_html: Si vous voulez rediriger ce compte vers un autre, vous pouvez le <a href="%{path}">configurer ici</a>.
or_log_in_with: Ou authentifiez-vous avec
privacy_policy_agreement_html: Jai lu et jaccepte la <a href="%{privacy_policy_path}" target="_blank">politique de confidentialité</a>
progress:
confirm: Confirmation de l'adresse mail
details: Vos infos

View File

@ -1135,7 +1135,6 @@ fr:
migrate_account: Déménager vers un compte différent
migrate_account_html: Si vous voulez rediriger ce compte vers un autre, vous pouvez le <a href="%{path}">configurer ici</a>.
or_log_in_with: Ou authentifiez-vous avec
privacy_policy_agreement_html: Jai lu et jaccepte la <a href="%{privacy_policy_path}" target="_blank">politique de confidentialité</a>
progress:
confirm: Confirmation de l'adresse mail
details: Vos infos

View File

@ -1117,7 +1117,6 @@ fy:
migrate_account: Nei in oar account ferhúzje
migrate_account_html: Wanneart jo dizze account nei in oare account trochferwize wolle, kinne jo <a href="%{path}">dit hjir ynstelle</a>.
or_log_in_with: Of oanmelde mei
privacy_policy_agreement_html: Ik haw it <a href="%{privacy_policy_path}" target="_blank">privacybelied</a> lêzen en gean dêrmei akkoard
progress:
confirm: E-mailadres werhelje
details: Jo gegevens

View File

@ -1186,7 +1186,6 @@ ga:
migrate_account: Bog chuig cuntas eile
migrate_account_html: Más mian leat an cuntas seo a atreorú chuig ceann eile, is féidir leat <a href="%{path}">é a chumrú anseo</a>.
or_log_in_with: Nó logáil isteach le
privacy_policy_agreement_html: Léigh mé agus aontaím leis an <a href="%{privacy_policy_path}" target="_blank">polasaí príobháideachais</a>
progress:
confirm: Deimhnigh ríomhphost
details: Do chuid sonraí

View File

@ -1168,7 +1168,6 @@ gd:
migrate_account: Imrich gu cunntas eile
migrate_account_html: Nam bu mhiann leat an cunntas seo ath-stiùireadh gu fear eile, s urrainn dhut <a href="%{path}">a rèiteachadh an-seo</a>.
or_log_in_with: No clàraich a-steach le
privacy_policy_agreement_html: Leugh mi is tha mi ag aontachadh ris a <a href="%{privacy_policy_path}" target="_blank">phoileasaidh prìobhaideachd</a>
progress:
confirm: Dearbh am post-d
details: Am fiosrachadh agad

View File

@ -1132,7 +1132,6 @@ gl:
migrate_account: Mover a unha conta diferente
migrate_account_html: Se queres redirixir esta conta hacia outra diferente, podes <a href="%{path}">facelo aquí</a>.
or_log_in_with: Ou accede con
privacy_policy_agreement_html: Lin e acepto a <a href="%{privacy_policy_path}" target="_blank">política de privacidade</a>
progress:
confirm: Confirmar correo
details: Detalles

View File

@ -1168,7 +1168,6 @@ he:
migrate_account: מעבר לחשבון אחר
migrate_account_html: אם ברצונך להכווין את החשבון לעבר חשבון אחר, ניתן <a href="%{path}">להגדיר זאת כאן</a>.
or_log_in_with: או התחבר באמצעות
privacy_policy_agreement_html: קראתי והסכמתי ל<a href="%{privacy_policy_path}" target="_blank">מדיניות הפרטיות</a>
progress:
confirm: אימות כתובת הדואל
details: הפרטים שלך

View File

@ -1132,7 +1132,6 @@ hu:
migrate_account: Felhasználói fiók költöztetése
migrate_account_html: Ha át szeretnéd irányítani ezt a fiókodat egy másikra, akkor <a href="%{path}">itt állíthatod be</a>.
or_log_in_with: Vagy jelentkezz be ezzel
privacy_policy_agreement_html: Elolvastam és egyetértek az <a href="%{privacy_policy_path}" target="_blank">adatvédemi nyilatkozattal</a>
progress:
confirm: E-mail megerősítése
details: Saját adatok

View File

@ -462,7 +462,6 @@ hy:
logout: Դուրս գալ
migrate_account: Տեղափոխուել այլ հաշիւ
or_log_in_with: Կամ մուտք գործել օգտագործելով՝
privacy_policy_agreement_html: Ես կարդացել եւ ընդունել եմ <a href="%{privacy_policy_path}" target="_blank">գաղնիութեան քաղաքականութիւնը</a>
progress:
details: Ձեր տուեալները
review: Վաւերացում

View File

@ -1132,7 +1132,6 @@ ia:
migrate_account: Migrar a un altere conto
migrate_account_html: Si tu vole rediriger iste conto a un altere, tu pote <a href="%{path}">configurar lo hic</a>.
or_log_in_with: O aperi session con
privacy_policy_agreement_html: Io ha legite e accepta le <a href="%{privacy_policy_path}" target="_blank">politica de confidentialitate</a>
progress:
confirm: Confirmar e-mail
details: Tu detalios

View File

@ -903,7 +903,6 @@ id:
migrate_account: Pindah ke akun berbeda
migrate_account_html: Jika Anda ingin mengalihkan akun ini ke akun lain, Anda dapat <a href="%{path}">mengaturnya di sini</a>.
or_log_in_with: Atau masuk dengan
privacy_policy_agreement_html: Saya telah membaca dan menerima <a href="%{privacy_policy_path}" target="_blank">kebijakan privasi</a>
providers:
cas: CAS
saml: SAML

View File

@ -1039,7 +1039,6 @@ ie:
migrate_account: Mover te a un conto diferent
migrate_account_html: Si tu vole redirecter ti-ci conto a un altri, tu posse <a href="%{path}">configurar it ci</a>.
or_log_in_with: O intrar med
privacy_policy_agreement_html: Yo leet e consenti li <a href="%{privacy_policy_path}" target="_blank">politica pri privatie</a>
progress:
details: Tui detallies
review: Nor revise

View File

@ -1014,7 +1014,6 @@ io:
migrate_account: Transferez a diferanta konto
migrate_account_html: Se vu volas ridirektar ca konto a diferanto, vu povas <a href="%{path}">ajustar hike</a>.
or_log_in_with: O eniras per
privacy_policy_agreement_html: Me lektis e konsentis <a href="%{privacy_policy_path}" target="_blank">privatesguidilo</a>
progress:
details: Vua detali
review: Nia revuo

View File

@ -1136,7 +1136,6 @@ is:
migrate_account: Færa á annan notandaaðgang
migrate_account_html: Ef þú vilt endurbeina þessum aðgangi á einhvern annan, geturðu <a href="%{path}">stillt það hér</a>.
or_log_in_with: Eða skráðu inn með
privacy_policy_agreement_html: Ég hef lesið og samþykkt <a href="%{privacy_policy_path}" target="_blank">persónuverndarstefnuna</a>
progress:
confirm: Staðfesta tölvupóstfang
details: Nánari upplýsingar þínar

View File

@ -1134,7 +1134,6 @@ it:
migrate_account: Sposta ad un account differente
migrate_account_html: Se vuoi che questo account sia reindirizzato a uno diverso, puoi <a href="%{path}">configurarlo qui</a>.
or_log_in_with: Oppure accedi con
privacy_policy_agreement_html: Ho letto e accetto l'<a href="%{privacy_policy_path}" target="_blank">informativa sulla privacy</a>
progress:
confirm: Conferma l'e-mail
details: I tuoi dettagli

View File

@ -1114,7 +1114,6 @@ ja:
migrate_account: 別のアカウントに引っ越す
migrate_account_html: 引っ越し先を明記したい場合は<a href="%{path}">こちら</a>で設定できます。
or_log_in_with: または次のサービスでログイン
privacy_policy_agreement_html: <a href="%{privacy_policy_path}" target="_blank">プライバシーポリシー</a>を読み、同意します
progress:
confirm: メールアドレスの確認
details: ユーザー情報

View File

@ -495,7 +495,6 @@ kab:
logout: Ffeɣ
migrate_account: Gujj ɣer umiḍan nniḍen
or_log_in_with: Neɣ eqqen s
privacy_policy_agreement_html: Ɣriɣ yerna qebleɣ <a href="%{privacy_policy_path}" target="_blank">tasertit n tbaḍnit</a>
progress:
confirm: Sentem imayl
details: Isalli-inek

View File

@ -1116,7 +1116,6 @@ ko:
migrate_account: 계정 옮기기
migrate_account_html: 이 계정을 다른 계정으로 리디렉션 하길 원하는 경우 <a href="%{path}">여기</a>에서 설정할 수 있습니다.
or_log_in_with: 다른 방법으로 로그인 하려면
privacy_policy_agreement_html: <a href="%{privacy_policy_path}" target="_blank">개인정보처리방침</a>을 읽고 동의합니다
progress:
confirm: 이메일 확인
details: 세부사항

View File

@ -916,7 +916,6 @@ ku:
migrate_account: Livandin bo ajimêreke din
migrate_account_html: Ku tu dixwazî ev ajimêr li ajimêreke cuda beralî bikî, tu dikarî <a href="%{path}">ji vir de saz bike</a>.
or_log_in_with: An têketinê bike bi riya
privacy_policy_agreement_html: Min <a href="%{privacy_policy_path}" target="_blank">Politîka taybetiyê</a> xwend û dipejirînim
providers:
cas: CAS
saml: SAML

View File

@ -1097,7 +1097,6 @@ lad:
migrate_account: Transferate a otro kuento
migrate_account_html: Si keres readresar este kuento a otra distinta, puedes <a href="%{path}">konfigurarlo aki</a>.
or_log_in_with: O konektate kon tu kuento kon
privacy_policy_agreement_html: Tengo meldado i acheto la <a href="%{privacy_policy_path}" target="_blank">politika de privasita</a>
progress:
confirm: Konfirma posta
details: Tus detalyos

View File

@ -795,7 +795,6 @@ lt:
migrate_account: Persikelti prie kitos paskyros
migrate_account_html: Jei nori šią paskyrą nukreipti į kitą, gali <a href="%{path}">sukonfigūruoti ją čia</a>.
or_log_in_with: Arba prisijungti su
privacy_policy_agreement_html: Perskaičiau ir sutinku su <a href="%{privacy_policy_path}" target="_blank">privatumo politika</a>
progress:
details: Tavo duomenys
review: Mūsų peržiūra

View File

@ -1110,7 +1110,6 @@ lv:
migrate_account: Pāriešana uz citu kontu
migrate_account_html: Ja vēlies novirzīt šo kontu uz citu, tu vari <a href="%{path}">to konfigurēt šeit</a>.
or_log_in_with: Vai piesakies ar
privacy_policy_agreement_html: Esmu izlasījis un piekrītu <a href="%{privacy_policy_path}" target="_blank">privātuma politikai</a>
progress:
confirm: Apstiprināt e-pasta adresi
details: Tavi dati

View File

@ -1001,7 +1001,6 @@ ms:
migrate_account: Pindah kepada akaun lain
migrate_account_html: Jika anda ingin mengubah hala akaun ini kepada akaun lain, anda boleh <a href="%{path}">konfigurasikannya di sini</a>.
or_log_in_with: Atau daftar masuk dengan
privacy_policy_agreement_html: Saya telah membaca dan bersetuju menerima <a href="%{privacy_policy_path}" target="_blank">dasar privasi</a>
progress:
details: Maklumat anda
review: Ulasan kami

View File

@ -994,7 +994,6 @@ my:
migrate_account: အခြားအကောင့်တစ်ခုသို့ ရွှေ့ရန်
migrate_account_html: ဤအကောင့်ကို အခြားအကောင့်သို့ ပြန်ညွှန်းလိုပါက <a href="%{path}">ဤနေရာတွင် စီစဉ်သတ်မှတ်နိုင်သည်</a>။
or_log_in_with: သို့မဟုတ် အကောင့်ဖြင့် ဝင်ရောက်ပါ
privacy_policy_agreement_html: <a href="%{privacy_policy_path}" target="_blank">ကိုယ်ရေးအချက်အလက်မူဝါဒ</a> ကို ဖတ်ပြီး သဘောတူလိုက်ပါပြီ
progress:
details: သင့်အသေးစိတ်အချက်အလက်များ
review: ကျွန်ုပ်တို့၏သုံးသပ်ချက်

View File

@ -1132,7 +1132,6 @@ nl:
migrate_account: Naar een ander account verhuizen
migrate_account_html: Wanneer je dit account naar een ander account wilt doorverwijzen, kun je <a href="%{path}">dit hier instellen</a>.
or_log_in_with: Of inloggen met
privacy_policy_agreement_html: Ik heb het <a href="%{privacy_policy_path}" target="_blank">privacybeleid</a> gelezen en ga daarmee akkoord
progress:
confirm: E-mailadres bevestigen
details: Jouw gegevens

View File

@ -1132,7 +1132,6 @@ nn:
migrate_account: Flytt til ein annan konto
migrate_account_html: Om du vil visa denne kontoen til ein anna, kan du <a href="%{path}">skipe det her</a>.
or_log_in_with: Eller logg inn med
privacy_policy_agreement_html: Jeg har lest og godtar <a href="%{privacy_policy_path}" target="_blank">retningslinjer for personvern</a>
progress:
confirm: Stadfest e-post
details: Opplysingane dine

View File

@ -1033,7 +1033,6 @@
migrate_account: Flytt til en annen konto
migrate_account_html: Hvis du ønsker å henvise denne kontoen til en annen, kan du <a href="%{path}">konfigurere det her</a>.
or_log_in_with: Eller logg inn med
privacy_policy_agreement_html: Jeg har lest og godtar <a href="%{privacy_policy_path}" target="_blank">retningslinjer for personvern</a>
progress:
details: Dine opplysninger
review: Vår gjennomgang

View File

@ -481,7 +481,6 @@ oc:
migrate_account: Mudar endacòm mai
migrate_account_html: Se volètz mandar los visitors daqueste compte a un autre, podètz<a href="%{path}"> o configurar aquí</a>.
or_log_in_with: O autentificatz-vos amb
privacy_policy_agreement_html: Ai legit e accepti la <a href="%{privacy_policy_path}" target="_blank">politica de confidencialitat</a>
providers:
cas: CAS
saml: SAML

View File

@ -1168,7 +1168,6 @@ pl:
migrate_account: Przenieś konto
migrate_account_html: Jeżeli chcesz skonfigurować przekierowanie z obecnego konta na inne, możesz <a href="%{path}">zrobić to tutaj</a>.
or_log_in_with: Lub zaloguj się z użyciem
privacy_policy_agreement_html: Przeczytałem/am i akceptuję <a href="%{privacy_policy_path}" target="_blank">politykę prywatności</a>
progress:
confirm: Potwierdź adres e-mail
details: Twoje dane

View File

@ -1132,7 +1132,6 @@ pt-BR:
migrate_account: Mudar-se para outra conta
migrate_account_html: Se você quer redirecionar essa conta para uma outra você pode <a href="%{path}">configurar isso aqui</a>.
or_log_in_with: Ou entre com
privacy_policy_agreement_html: Eu li e concordo com a <a href="%{privacy_policy_path}" target="_blank">política de privacidade</a>
progress:
confirm: Confirmar e-mail
details: Suas informações

View File

@ -1113,7 +1113,6 @@ pt-PT:
migrate_account: Mudar para uma conta diferente
migrate_account_html: Se deseja redirecionar esta conta para uma outra pode <a href="%{path}">configurar isso aqui</a>.
or_log_in_with: Ou iniciar sessão com
privacy_policy_agreement_html: Eu li e concordo com a <a href="%{privacy_policy_path}" target="_blank">política de privacidade</a>
progress:
confirm: Confirmar e-mail
details: Os seus dados

View File

@ -1168,7 +1168,6 @@ ru:
migrate_account: Перенос учётной записи
migrate_account_html: Завели новую учётную запись? Перенаправьте подписчиков на неё — <a href="%{path}">настройте перенаправление тут</a>.
or_log_in_with: Или войти с помощью
privacy_policy_agreement_html: Мной прочитана и принята <a href="%{privacy_policy_path}" target="_blank">политика конфиденциальности</a>
progress:
confirm: Подтвердите электронную почту
details: Ваши данные

View File

@ -909,7 +909,6 @@ sco:
migrate_account: Uise a different accoont
migrate_account_html: Gin ye'r wantin fir tae redireck this accoont tae a different ane, ye kin <a href="%{path}">configure it here</a>.
or_log_in_with: Or log in wi
privacy_policy_agreement_html: A'v read an A agree tae the <a href="%{privacy_policy_path}" target="_blank">privacy policy</a>
providers:
cas: CAS
saml: SAML

View File

@ -130,6 +130,17 @@ en:
show_application: You will always be able to see which app published your post regardless.
tag:
name: You can only change the casing of the letters, for example, to make it more readable
terms_of_service:
changelog: Can be structured with Markdown syntax.
text: Can be structured with Markdown syntax.
terms_of_service_generator:
admin_email: Legal notices include counternotices, court orders, takedown requests, and law enforcement requests.
arbitration_address: Can be the same as Physical address above, or “N/A” if using email
arbitration_website: Can be a web form, or “N/A” if using email
dmca_address: For US operators, use the address registered in the DMCA Designated Agent Directory. A P.O. Box listing is available upon direct request, use the DMCA Designated Agent Post Office Box Waiver Request to email the Copyright Office and describe that you are a home-based content moderator who fears revenge or retribution for your actions and need to use a P.O. Box to remove your home address from public view.
dmca_email: Can be the same email used for “Email address for legal notices” above
domain: Unique identification of the online service you are providing.
jurisdiction: List the country where whoever pays the bills lives. If its a company or other entity, list the country where its incorporated, and the city, region, territory or state as appropriate.
user:
chosen_languages: When checked, only posts in selected languages will be displayed in public timelines
role: The role controls which permissions the user has.
@ -319,6 +330,17 @@ en:
name: Hashtag
trendable: Allow this hashtag to appear under trends
usable: Allow posts to use this hashtag locally
terms_of_service:
changelog: What's changed?
text: Terms of Service
terms_of_service_generator:
admin_email: Email address for legal notices
arbitration_address: Physical address for arbitration notices
arbitration_website: Website for submitting arbitration notices
dmca_address: Physical address for DMCA/copyright notices
dmca_email: Email address for DMCA/copyright notices
domain: Domain
jurisdiction: Legal jurisdiction
user:
role: Role
time_zone: Time zone

View File

@ -1141,7 +1141,6 @@ sl:
migrate_account: Premakni se na drug račun
migrate_account_html: Če želite ta račun preusmeriti na drugega, ga lahko <a href="%{path}">nastavite tukaj</a>.
or_log_in_with: Ali se prijavite z
privacy_policy_agreement_html: Prebral_a sem in se strinjam s <a href="%{privacy_policy_path}" target="_blank">pravilnikom o zasebnosti</a>.
progress:
confirm: Potrdi e-pošto
details: Vaši podatki

Some files were not shown because too many files have changed in this diff Show More