Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
	app/controllers/application_controller.rb

Changed instance theme selection by instance flavour selection.
This commit is contained in:
Thibaut Girka
2018-08-24 13:34:51 +02:00
52 changed files with 534 additions and 282 deletions

View File

@ -16,6 +16,8 @@ module Admin
timeline_preview
show_staff_badge
bootstrap_timeline_accounts
flavour
skin
thumbnail
hero
min_invite_role
@ -23,6 +25,7 @@ module Admin
peers_api_enabled
show_known_fediverse_at_about_page
preview_sensitive_media
custom_css
).freeze
BOOLEAN_SETTINGS = %w(

View File

@ -14,7 +14,7 @@ module Admin
@suspension = Form::AdminSuspensionConfirmation.new(suspension_params)
if suspension_params[:acct] == @account.acct
resolve_report! if suspension_params[:report_id]
resolve_report! if suspension_params[:report_id].present?
perform_suspend!
mark_reports_resolved!
redirect_to admin_accounts_path

View File

@ -7,6 +7,8 @@ class Api::BaseController < ApplicationController
include RateLimitHeaders
skip_before_action :store_current_location
skip_before_action :check_user_permissions
protect_from_forgery with: :null_session
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|

View File

@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base
rescue_from Mastodon::NotPermittedError, with: :forbidden
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
before_action :check_suspension, if: :user_signed_in?
before_action :check_user_permissions, if: :user_signed_in?
def raise_not_found
raise ActionController::RoutingError, "No route matches #{params[:unmatched_route]}"
@ -49,8 +49,8 @@ class ApplicationController < ActionController::Base
forbidden unless current_user&.staff?
end
def check_suspension
forbidden if current_user.account.suspended?
def check_user_permissions
forbidden if current_user.disabled? || current_user.account.suspended?
end
def after_sign_out_path_for(_resource_or_scope)
@ -165,12 +165,12 @@ class ApplicationController < ActionController::Base
end
def current_flavour
return Setting.default_settings['flavour'] unless Themes.instance.flavours.include? current_user&.setting_flavour
return Setting.flavour unless Themes.instance.flavours.include? current_user&.setting_flavour
current_user.setting_flavour
end
def current_skin
return 'default' unless Themes.instance.skins_for(current_flavour).include? current_user&.setting_skin
return Setting.skin unless Themes.instance.skins_for(current_flavour).include? current_user&.setting_skin
current_user.setting_skin
end

View File

@ -6,7 +6,7 @@ class Auth::SessionsController < Devise::SessionsController
layout 'auth'
skip_before_action :require_no_authentication, only: [:create]
skip_before_action :check_suspension, only: [:destroy]
skip_before_action :check_user_permissions, only: [:destroy]
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
prepend_before_action :set_pack
before_action :set_instance_presenter, only: [:new]

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
class CustomCssController < ApplicationController
before_action :set_cache_headers
def show
skip_session!
render plain: Setting.custom_css || '', content_type: 'text/css'
end
end

View File

@ -130,7 +130,7 @@ export function submitCompose() {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
},
}).then(function (response) {
dispatch(insertIntoTagHistory(response.data.tags));
dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately get the status into the columns
@ -390,13 +390,13 @@ export function hydrateCompose() {
};
}
function insertIntoTagHistory(tags) {
function insertIntoTagHistory(recognizedTags, text) {
return (dispatch, getState) => {
const state = getState();
const oldHistory = state.getIn(['compose', 'tagHistory']);
const me = state.getIn(['meta', 'me']);
const names = tags.map(({ name }) => name);
const intersectedOldHistory = oldHistory.filter(name => !names.includes(name));
const names = recognizedTags.map(tag => text.match(new RegExp(`#${tag.name}`, 'i'))[0].slice(1));
const intersectedOldHistory = oldHistory.filter(name => names.findIndex(newName => newName.toLowerCase() === name.toLowerCase()) === -1);
names.push(...intersectedOldHistory.toJS());

View File

@ -7,6 +7,7 @@ export default class Column extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
label: PropTypes.string,
};
scrollTop () {
@ -40,10 +41,10 @@ export default class Column extends React.PureComponent {
}
render () {
const { children } = this.props;
const { label, children } = this.props;
return (
<div role='region' className='column' ref={this.setRef}>
<div role='region' aria-label={label} className='column' ref={this.setRef}>
{children}
</div>
);

View File

@ -226,6 +226,12 @@ export default class Dropdown extends React.PureComponent {
return this.target;
}
componentWillUnmount = () => {
if (this.state.id === this.props.openDropdownId) {
this.handleClose();
}
}
render () {
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId } = this.props;
const open = this.state.id === openDropdownId;

View File

@ -109,7 +109,7 @@ export default class IntersectionObserverArticle extends React.Component {
return (
<article
ref={this.handleRef}
aria-posinset={index}
aria-posinset={index + 1}
aria-setsize={listLength}
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
data-id={id}
@ -121,7 +121,7 @@ export default class IntersectionObserverArticle extends React.Component {
}
return (
<article ref={this.handleRef} aria-posinset={index} aria-setsize={listLength} data-id={id} tabIndex='0'>
<article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex='0'>
{children && React.cloneElement(children, { hidden: false })}
</article>
);

View File

@ -8,7 +8,7 @@ import DisplayName from './display_name';
import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list';
import { FormattedMessage } from 'react-intl';
import { injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video } from '../features/ui/util/async-components';
import { HotKeys } from 'react-hotkeys';
@ -18,6 +18,24 @@ import classNames from 'classnames';
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
export const textForScreenReader = (intl, status, rebloggedByText = false, expanded = false) => {
const displayName = status.getIn(['account', 'display_name']);
const values = [
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
status.get('spoiler_text') && !expanded ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length),
intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
status.getIn(['account', 'acct']),
];
if (rebloggedByText) {
values.push(rebloggedByText);
}
return values.join(', ');
};
@injectIntl
export default class Status extends ImmutablePureComponent {
static contextTypes = {
@ -138,9 +156,9 @@ export default class Status extends ImmutablePureComponent {
render () {
let media = null;
let statusAvatar, prepend;
let statusAvatar, prepend, rebloggedByText;
const { hidden, featured } = this.props;
const { intl, hidden, featured } = this.props;
let { status, account, ...other } = this.props;
@ -189,6 +207,8 @@ export default class Status extends ImmutablePureComponent {
</div>
);
rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: status.getIn(['account', 'acct']) });
account = status.get('account');
status = status.get('reblog');
}
@ -248,7 +268,7 @@ export default class Status extends ImmutablePureComponent {
return (
<HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}>
{prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { muted: this.props.muted })} data-id={status.get('id')}>

View File

@ -105,7 +105,7 @@ export default class CommunityTimeline extends React.PureComponent {
const pinned = !!columnId;
return (
<Column ref={this.setRef}>
<Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='users'
active={hasUnread}

View File

@ -22,6 +22,7 @@ const messages = defineMessages({
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new toot' },
});
const mapStateToProps = (state, ownProps) => ({
@ -95,7 +96,7 @@ export default class Compose extends React.PureComponent {
}
return (
<div className='drawer'>
<div className='drawer' role='region' aria-label={intl.formatMessage(messages.compose)}>
{header}
{(multiColumn || isSearchPage) && <SearchContainer /> }

View File

@ -76,7 +76,7 @@ export default class DirectTimeline extends React.PureComponent {
const pinned = !!columnId;
return (
<Column ref={this.setRef}>
<Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='envelope'
active={hasUnread}

View File

@ -72,7 +72,7 @@ export default class Favourites extends ImmutablePureComponent {
const pinned = !!columnId;
return (
<Column ref={this.setRef}>
<Column ref={this.setRef} label={intl.formatMessage(messages.heading)}>
<ColumnHeader
icon='star'
title={intl.formatMessage(messages.heading)}

View File

@ -31,6 +31,7 @@ const messages = defineMessages({
discover: { id: 'navigation_bar.discover', defaultMessage: 'Discover' },
personal: { id: 'navigation_bar.personal', defaultMessage: 'Personal' },
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
});
const mapStateToProps = state => ({
@ -115,7 +116,7 @@ export default class GettingStarted extends ImmutablePureComponent {
}
return (
<Column>
<Column label={intl.formatMessage(messages.menu)}>
{multiColumn && <div className='column-header__wrapper'>
<h1 className='column-header'>
<button>

View File

@ -89,7 +89,7 @@ export default class HashtagTimeline extends React.PureComponent {
const pinned = !!columnId;
return (
<Column ref={this.setRef}>
<Column ref={this.setRef} label={`#${id}`}>
<ColumnHeader
icon='hashtag'
active={hasUnread}

View File

@ -98,7 +98,7 @@ export default class HomeTimeline extends React.PureComponent {
const pinned = !!columnId;
return (
<Column ref={this.setRef}>
<Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='home'
active={hasUnread}

View File

@ -137,7 +137,7 @@ export default class ListTimeline extends React.PureComponent {
}
return (
<Column ref={this.setRef}>
<Column ref={this.setRef} label={title}>
<ColumnHeader
icon='list-ul'
active={hasUnread}

View File

@ -165,7 +165,7 @@ export default class Notifications extends React.PureComponent {
);
return (
<Column ref={this.setColumnRef}>
<Column ref={this.setColumnRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='bell'
active={isUnread}

View File

@ -112,7 +112,7 @@ export default class PublicTimeline extends React.PureComponent {
const pinned = !!columnId;
return (
<Column ref={this.setRef}>
<Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='globe'
active={hasUnread}

View File

@ -51,7 +51,7 @@ export default class CommunityTimeline extends React.PureComponent {
const { intl } = this.props;
return (
<Column ref={this.setRef}>
<Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='users'
title={intl.formatMessage(messages.title)}

View File

@ -51,7 +51,7 @@ export default class PublicTimeline extends React.PureComponent {
const { intl } = this.props;
return (
<Column ref={this.setRef}>
<Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='globe'
title={intl.formatMessage(messages.title)}

View File

@ -43,6 +43,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal } from '../../initial_state';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
import { textForScreenReader } from '../../components/status';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@ -52,6 +53,7 @@ const messages = defineMessages({
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
});
const makeMapStateToProps = () => {
@ -404,7 +406,7 @@ export default class Status extends ImmutablePureComponent {
};
return (
<Column>
<Column label={intl.formatMessage(messages.detailedStatus)}>
<ColumnHeader
showBackButton
extraButton={(
@ -417,7 +419,7 @@ export default class Status extends ImmutablePureComponent {
{ancestors}
<HotKeys handlers={handlers}>
<div className='focusable' tabIndex='0'>
<div className='focusable' tabIndex='0' aria-label={textForScreenReader(intl, status, false, !status.get('hidden'))}>
<DetailedStatus
status={status}
onOpenVideo={this.handleOpenVideo}

View File

@ -1,66 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { injectIntl, defineMessages } from 'react-intl';
import Column from '../ui/components/column';
import ColumnHeader from '../../components/column_header';
import Hashtag from '../../components/hashtag';
import classNames from 'classnames';
import { fetchTrends } from '../../actions/trends';
const messages = defineMessages({
title: { id: 'trends.header', defaultMessage: 'Trending now' },
refreshTrends: { id: 'trends.refresh', defaultMessage: 'Refresh trends' },
});
const mapStateToProps = state => ({
trends: state.getIn(['trends', 'items']),
loading: state.getIn(['trends', 'isLoading']),
});
const mapDispatchToProps = dispatch => ({
fetchTrends: () => dispatch(fetchTrends()),
});
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
export default class Trends extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
trends: ImmutablePropTypes.list,
fetchTrends: PropTypes.func.isRequired,
loading: PropTypes.bool,
};
componentDidMount () {
this.props.fetchTrends();
}
handleRefresh = () => {
this.props.fetchTrends();
}
render () {
const { trends, loading, intl } = this.props;
return (
<Column>
<ColumnHeader
icon='fire'
title={intl.formatMessage(messages.title)}
extraButton={(
<button className='column-header__button' title={intl.formatMessage(messages.refreshTrends)} aria-label={intl.formatMessage(messages.refreshTrends)} onClick={this.handleRefresh}><i className={classNames('fa', 'fa-refresh', { 'fa-spin': loading })} /></button>
)}
/>
<div className='scrollable'>
{trends && trends.map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
</div>
</Column>
);
}
}

View File

@ -22,7 +22,7 @@
"account.posts": "Pouets",
"account.posts_with_replies": "Pouets et réponses",
"account.report": "Signaler",
"account.requested": "En attente d'approbation. Cliquez pour annuler la requête",
"account.requested": "En attente dapprobation. Cliquez pour annuler la requête",
"account.share": "Partager le profil de @{name}",
"account.show_reblogs": "Afficher les partages de @{name}",
"account.unblock": "Débloquer",
@ -32,7 +32,7 @@
"account.unmute": "Ne plus masquer",
"account.unmute_notifications": "Réactiver les notifications de @{name}",
"account.view_full_profile": "Afficher le profil complet",
"alert.unexpected.message": "Une erreur non-attendue s'est produite.",
"alert.unexpected.message": "Une erreur non attendue sest produite.",
"alert.unexpected.title": "Oups!",
"boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
"bundle_column_error.body": "Une erreur sest produite lors du chargement de ce composant.",
@ -62,9 +62,9 @@
"column_header.unpin": "Retirer",
"column_subheading.settings": "Paramètres",
"community.column_settings.media_only": "Média uniquement",
"compose_form.direct_message_warning": "Ce pouet sera uniquement envoyé qu'aux personnes mentionnées. Cependant, l'administration de votre instance et des instances réceptrices pourront inspecter ce message.",
"compose_form.direct_message_warning": "Ce pouet sera uniquement envoyé aux personnes mentionnées. Cependant, ladministration de votre instance et des instances réceptrices pourront inspecter ce message.",
"compose_form.direct_message_warning_learn_more": "En savoir plus",
"compose_form.hashtag_warning": "Ce pouet ne sera pas listé dans les recherches par hashtag car sa visibilité est réglée sur \"non-listé\". Seuls les pouets avec une visibilité \"publique\" peuvent être recherchés par hashtag.",
"compose_form.hashtag_warning": "Ce pouet ne sera pas listé dans les recherches par hashtag car sa visibilité est réglée sur \"non listé\". Seuls les pouets avec une visibilité \"publique\" peuvent être recherchés par hashtag.",
"compose_form.lock_disclaimer": "Votre compte nest pas {locked}. Tout le monde peut vous suivre et voir vos pouets privés.",
"compose_form.lock_disclaimer.lock": "verrouillé",
"compose_form.placeholder": "Quavez-vous en tête?",
@ -73,7 +73,7 @@
"compose_form.sensitive.marked": "Média marqué comme sensible",
"compose_form.sensitive.unmarked": "Média non marqué comme sensible",
"compose_form.spoiler.marked": "Le texte est caché derrière un avertissement",
"compose_form.spoiler.unmarked": "Le texte n'est pas caché",
"compose_form.spoiler.unmarked": "Le texte nest pas caché",
"compose_form.spoiler_placeholder": "Écrivez ici votre avertissement",
"confirmation_modal.cancel": "Annuler",
"confirmations.block.confirm": "Bloquer",
@ -83,11 +83,11 @@
"confirmations.delete_list.confirm": "Supprimer",
"confirmations.delete_list.message": "Êtes-vous sûr de vouloir supprimer définitivement cette liste?",
"confirmations.domain_block.confirm": "Masquer le domaine entier",
"confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables. Vous ne verrez plus de contenu provenant de ce domaine ni dans vos lignes de temps publiques, ni dans vos notifications. Vos suiveurs utilisant ce domaine seront retirés.",
"confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables. Vous ne verrez plus de contenu provenant de ce domaine, ni dans fils publics, ni dans vos notifications. Vos abonné·e·s utilisant ce domaine seront retiré·e·s.",
"confirmations.mute.confirm": "Masquer",
"confirmations.mute.message": "Confirmez-vous le masquage de {name}?",
"confirmations.redraft.confirm": "Effacer et ré-écrire",
"confirmations.redraft.message": "Êtes vous sûr de vouloir effacer ce statut pour le ré-écrire ? Vous perdrez toutes ses réponses, ses repartages et ses mises en favori.",
"confirmations.redraft.message": "Êtes-vous sûr·e de vouloir effacer ce statut pour le ré-écrire? Vous perdrez toutes ses réponses, ses repartages et ses mises en favori.",
"confirmations.unfollow.confirm": "Ne plus suivre",
"confirmations.unfollow.message": "Voulez-vous arrêter de suivre {name}?",
"embed.instructions": "Intégrez ce statut à votre site en copiant le code ci-dessous.",
@ -98,7 +98,7 @@
"emoji_button.food": "Nourriture & Boisson",
"emoji_button.label": "Insérer un émoji",
"emoji_button.nature": "Nature",
"emoji_button.not_found": "Pas d'emojis!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "Pas d’émoji!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objets",
"emoji_button.people": "Personnages",
"emoji_button.recent": "Fréquemment utilisés",
@ -107,11 +107,11 @@
"emoji_button.symbols": "Symboles",
"emoji_button.travel": "Lieux & Voyages",
"empty_column.community": "Le fil public local est vide. Écrivez donc quelque chose pour le remplir!",
"empty_column.direct": "Vous n'avez pas encore de messages directs. Lorsque vous en enverrez ou recevrez un, il s'affichera ici.",
"empty_column.direct": "Vous navez pas encore de messages directs. Lorsque vous en enverrez ou recevrez un, il saffichera ici.",
"empty_column.hashtag": "Il ny a encore aucun contenu associé à ce hashtag.",
"empty_column.home": "Vous ne suivez personne. Visitez {public} ou utilisez la recherche pour trouver dautres personnes à suivre.",
"empty_column.home.public_timeline": "le fil public",
"empty_column.list": "Il n'y a rien dans cette liste pour l'instant. Dès que des personnes de cette liste publierons de nouveaux statuts, ils apparaîtront ici.",
"empty_column.list": "Il ny a rien dans cette liste pour linstant. Dès que des personnes de cette liste publieront de nouveaux statuts, ils apparaîtront ici.",
"empty_column.notifications": "Vous navez pas encore de notification. Interagissez avec dautres personnes pour débuter la conversation.",
"empty_column.public": "Il ny a rien ici! Écrivez quelque chose publiquement, ou bien suivez manuellement des personnes dautres instances pour remplir le fil public",
"follow_request.authorize": "Accepter",
@ -129,7 +129,7 @@
"home.column_settings.show_replies": "Afficher les réponses",
"keyboard_shortcuts.back": "revenir en arrière",
"keyboard_shortcuts.boost": "partager",
"keyboard_shortcuts.column": "focaliser un statut dans l'une des colonnes",
"keyboard_shortcuts.column": "focaliser un statut dans lune des colonnes",
"keyboard_shortcuts.compose": "pour centrer la zone de rédaction",
"keyboard_shortcuts.description": "Description",
"keyboard_shortcuts.down": "pour descendre dans la liste",
@ -138,8 +138,8 @@
"keyboard_shortcuts.heading": "Raccourcis clavier",
"keyboard_shortcuts.hotkey": "Raccourci",
"keyboard_shortcuts.legend": "pour afficher cette légende",
"keyboard_shortcuts.mention": "pour mentionner l'auteur",
"keyboard_shortcuts.profile": "pour ouvrir le profil de l'auteur",
"keyboard_shortcuts.mention": "pour mentionner lauteur·rice",
"keyboard_shortcuts.profile": "pour ouvrir le profil de lauteur·rice",
"keyboard_shortcuts.reply": "pour répondre",
"keyboard_shortcuts.search": "pour cibler la recherche",
"keyboard_shortcuts.toggle_hidden": "pour afficher/cacher un texte derrière CW",
@ -202,12 +202,12 @@
"onboarding.next": "Suivant",
"onboarding.page_five.public_timelines": "Le fil public global affiche les messages de toutes les personnes suivies par les membres de {domain}. Le fil public local est identique, mais se limite aux membres de {domain}.",
"onboarding.page_four.home": "Laccueil affiche les messages des personnes que vous suivez.",
"onboarding.page_four.notifications": "La colonne de notification vous avertit lors d'une interaction avec vous.",
"onboarding.page_four.notifications": "La colonne de notification vous avertit lors dune interaction avec vous.",
"onboarding.page_one.federation": "Mastodon est un réseau de serveurs indépendants qui se joignent pour former un réseau social plus vaste. Nous appelons ces serveurs des instances.",
"onboarding.page_one.full_handle": "Votre identifiant complet",
"onboarding.page_one.handle_hint": "C'est ce que vos amis devront rechercher.",
"onboarding.page_one.handle_hint": "Cest ce que vos ami·e·s devront rechercher.",
"onboarding.page_one.welcome": "Bienvenue sur Mastodon!",
"onboarding.page_six.admin": "Ladministrateur⋅ice de votre instance est {admin}.",
"onboarding.page_six.admin": "Votre instance est administrée par {admin}.",
"onboarding.page_six.almost_done": "Nous y sommes presque…",
"onboarding.page_six.appetoot": "Bon appouétit!",
"onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres.",
@ -220,14 +220,14 @@
"onboarding.page_two.compose": "Écrivez depuis la colonne de composition. Vous pouvez ajouter des images, changer les réglages de confidentialité, et ajouter des avertissements de contenu (Content Warning) grâce aux icônes en dessous.",
"onboarding.skip": "Passer",
"privacy.change": "Ajuster la confidentialité du message",
"privacy.direct.long": "N'envoyer qu'aux personnes mentionnées",
"privacy.direct.long": "Nenvoyer quaux personnes mentionnées",
"privacy.direct.short": "Direct",
"privacy.private.long": "Seul⋅e⋅s vos abonné⋅e⋅s verront vos statuts",
"privacy.private.short": "Abonné⋅e⋅s uniquement",
"privacy.public.long": "Afficher dans les fils publics",
"privacy.public.short": "Public",
"privacy.unlisted.long": "Ne pas afficher dans les fils publics",
"privacy.unlisted.short": "Non-listé",
"privacy.unlisted.short": "Non listé",
"regeneration_indicator.label": "Chargement…",
"regeneration_indicator.sublabel": "Le flux de votre page principale est en cours de préparation!",
"relative_time.days": "{number} j",
@ -237,8 +237,8 @@
"relative_time.seconds": "{number} s",
"reply_indicator.cancel": "Annuler",
"report.forward": "Transférer à {target}",
"report.forward_hint": "Le compte provient d'un autre serveur. Envoyez également une copie anonyme du rapport?",
"report.hint": "Le rapport sera envoyé aux modérateurs de votre instance. Vous pouvez expliquer pourquoi vous signalez le compte ci-dessous:",
"report.forward_hint": "Le compte provient dun autre serveur. Envoyez également une copie anonyme du rapport?",
"report.hint": "Le rapport sera envoyé aux modérateur·rice·s de votre instance. Vous pouvez expliquer pourquoi vous signalez le compte ci-dessous:",
"report.placeholder": "Commentaires additionnels",
"report.submit": "Envoyer",
"report.target": "Signalement",
@ -272,7 +272,7 @@
"status.pin": "Épingler sur le profil",
"status.pinned": "Pouet épinglé",
"status.reblog": "Partager",
"status.reblog_private": "Booster vers l'audience originale",
"status.reblog_private": "Booster vers laudience originale",
"status.reblogged_by": "{name} a partagé:",
"status.redraft": "Effacer et ré-écrire",
"status.reply": "Répondre",
@ -292,16 +292,16 @@
"tabs_bar.local_timeline": "Fil public local",
"tabs_bar.notifications": "Notifications",
"tabs_bar.search": "Chercher",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} discutent",
"trends.count_by_accounts": "{count} {rawCount, plural, one {personne} other {personnes}} discutent",
"ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.",
"upload_area.title": "Glissez et déposez pour envoyer",
"upload_button.label": "Joindre un média",
"upload_form.description": "Décrire pour les malvoyants",
"upload_form.description": "Décrire pour les malvoyant·e·s",
"upload_form.focus": "Recadrer",
"upload_form.undo": "Supprimer",
"upload_progress.label": "Envoi en cours…",
"video.close": "Fermer la vidéo",
"video.exit_fullscreen": "Quitter plein écran",
"video.exit_fullscreen": "Quitter le plein écran",
"video.expand": "Agrandir la vidéo",
"video.fullscreen": "Plein écran",
"video.hide": "Masquer la vidéo",

View File

@ -131,7 +131,7 @@ const updateSuggestionTags = (state, token) => {
return state.merge({
suggestions: state.get('tagHistory')
.filter(tag => tag.startsWith(prefix))
.filter(tag => tag.toLowerCase().startsWith(prefix.toLowerCase()))
.slice(0, 4)
.map(tag => '#' + tag),
suggestion_token: token,

View File

@ -80,15 +80,7 @@ const handlePush = (event) => {
// Placeholder until more information can be loaded
event.waitUntil(
notify({
title,
body,
icon,
tag: notification_id,
timestamp: new Date(),
badge: '/badge.png',
data: { access_token, preferred_locale, url: '/web/notifications' },
}).then(() => fetchFromApi(`/api/v1/notifications/${notification_id}`, 'get', access_token)).then(notification => {
fetchFromApi(`/api/v1/notifications/${notification_id}`, 'get', access_token).then(notification => {
const options = {};
options.title = formatMessage(`notification.${notification.type}`, preferred_locale, { name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
@ -112,6 +104,16 @@ const handlePush = (event) => {
}
return notify(options);
}).catch(() => {
return notify({
title,
body,
icon,
tag: notification_id,
timestamp: new Date(),
badge: '/badge.png',
data: { access_token, preferred_locale, url: '/web/notifications' },
});
})
);
};

View File

@ -169,6 +169,10 @@
color: $white;
}
.dropdown-menu__separator {
border-bottom-color: lighten($ui-base-color, 12%);
}
// Change the background colors of modals
.actions-modal,
.boost-modal,
@ -281,3 +285,87 @@
}
}
}
.flash-message {
box-shadow: none;
&.notice {
background: rgba($success-green, 0.5);
color: lighten($success-green, 12%);
}
&.alert {
background: rgba($error-red, 0.5);
color: lighten($error-red, 12%);
}
}
.simple_form,
.table-form {
.warning {
box-shadow: none;
background: rgba($error-red, 0.5);
text-shadow: none;
}
}
.status__content,
.reply-indicator__content {
a {
color: $highlight-text-color;
}
}
.button.logo-button {
color: $white;
svg path:first-child {
fill: $white;
}
}
.public-layout {
.header,
.public-account-header,
.public-account-bio {
box-shadow: none;
}
.header {
background: lighten($ui-base-color, 12%);
}
.public-account-header {
&__image {
background: lighten($ui-base-color, 12%);
&::after {
box-shadow: none;
}
}
&__tabs {
&__name {
h1,
h1 small {
color: $white;
}
}
}
}
}
.account__section-headline a.active::after {
border-color: transparent transparent $white;
}
.hero-widget,
.box-widget,
.contact-widget,
.landing-page__information.contact-widget,
.moved-account-widget,
.memoriam-widget,
.activity-stream,
.nothing-here {
box-shadow: none;
}

View File

@ -621,3 +621,14 @@ code {
.scope-danger {
color: $warning-red;
}
.form_admin_settings_site_short_description,
.form_admin_settings_site_description,
.form_admin_settings_site_extended_description,
.form_admin_settings_site_terms,
.form_admin_settings_custom_css,
.form_admin_settings_closed_registrations_message {
textarea {
font-family: 'mastodon-font-monospace', monospace;
}
}

View File

@ -107,7 +107,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
updated = tag['updated']
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
return unless emoji.nil? || emoji.updated_at >= updated
return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && emoji.updated_at >= updated)
emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
emoji.image_remote_url = image_url

View File

@ -2,6 +2,11 @@
module Settings
class ScopedSettings
DEFAULTING_TO_UNSCOPED = %w(
flavour
skin
).freeze
def initialize(object)
@object = object
end
@ -50,15 +55,22 @@ module Settings
Rails.cache.fetch(Setting.cache_key(key, @object)) do
db_val = thing_scoped.find_by(var: key.to_s)
if db_val
default_value = Setting.default_settings[key]
default_value = ScopedSettings.default_settings[key]
return default_value.with_indifferent_access.merge!(db_val.value) if default_value.is_a?(Hash)
db_val.value
else
Setting.default_settings[key]
ScopedSettings.default_settings[key]
end
end
end
class << self
def default_settings
defaulting = DEFAULTING_TO_UNSCOPED.map { |k| [k, Setting[k]] }.to_h
Setting.default_settings.merge!(defaulting)
end
end
protected
def thing_scoped

View File

@ -30,6 +30,10 @@ class Form::AdminSettings
:show_staff_badge=,
:bootstrap_timeline_accounts,
:bootstrap_timeline_accounts=,
:flavour,
:flavour=,
:skin,
:skin=,
:min_invite_role,
:min_invite_role=,
:activity_api_enabled,
@ -40,6 +44,8 @@ class Form::AdminSettings
:show_known_fediverse_at_about_page=,
:preview_sensitive_media,
:preview_sensitive_media=,
:custom_css,
:custom_css=,
to: Setting
)
end

View File

@ -216,10 +216,6 @@ class User < ApplicationRecord
save!
end
def active_for_authentication?
super && !disabled?
end
def setting_default_privacy
settings.default_privacy || (account.locked? ? 'private' : 'public')
end

View File

@ -18,11 +18,11 @@ class UserPolicy < ApplicationPolicy
end
def enable?
admin?
staff?
end
def disable?
admin? && !record.admin?
staff? && !record.admin?
end
def promote?

View File

@ -14,7 +14,7 @@ class InstancePresenter
)
def contact_account
Account.find_local(Setting.site_contact_username)
Account.find_local(Setting.site_contact_username.gsub(/\A@/, ''))
end
def user_count

View File

@ -93,11 +93,11 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
end
def avatar_exists?
object.avatar.exists?
object.avatar?
end
def header_exists?
object.header.exists?
object.header?
end
def manually_approves_followers

View File

@ -33,7 +33,7 @@ class Web::NotificationSerializer < ActiveModel::Serializer
end
def body
str = truncate(strip_tags(object.target_status&.spoiler_text&.presence || object.target_status&.text || object.from_account.note), length: 140)
HTMLEntities.new.decode(str.to_str) # Do not encode entities, since this value will not be used in HTML
str = strip_tags(object.target_status&.spoiler_text&.presence || object.target_status&.text || object.from_account.note)
truncate(HTMLEntities.new.decode(str.to_str), length: 140) # Do not encode entities, since this value will not be used in HTML
end
end

View File

@ -226,7 +226,7 @@ class ActivityPub::ProcessAccountService < BaseService
updated = tag['updated']
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
return unless emoji.nil? || emoji.updated_at >= updated
return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && emoji.updated_at >= updated)
emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
emoji.image_remote_url = image_url

View File

@ -15,6 +15,7 @@
%hr/
.fields-group
= f.input :flavour, collection: Themes.instance.flavours, label_method: lambda { |flavour| I18n.t("flavours.#{flavour}.name", default: flavour) }, wrapper: :with_label, include_blank: false
= f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: t('admin.settings.thumbnail.desc_html')
= f.input :hero, as: :file, wrapper: :with_block_label, label: t('admin.settings.hero.title'), hint: t('admin.settings.hero.desc_html')
@ -48,7 +49,7 @@
.fields-group
= f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 }
= f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 }
= f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }, label: t('admin.settings.custom_css.title'), hint: t('admin.settings.custom_css.desc_html')
%hr/
.fields-group

View File

@ -3,7 +3,7 @@
%li= link_to t('auth.login'), new_session_path(resource_name)
- if devise_mapping.registerable? && controller_name != 'registrations'
%li= link_to t('auth.register'), new_registration_path(resource_name)
%li= link_to t('auth.register'), open_registrations? ? new_registration_path(resource_name) : 'https://joinmastodon.org/#getting-started'
- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations'
%li= link_to t('auth.forgot_password'), new_password_path(resource_name)

View File

@ -21,6 +21,9 @@
= javascript_pack_tag "locales/#{@theme[:flavour]}/en", integrity: true, crossorigin: 'anonymous'
= csrf_meta_tags
- if Setting.custom_css.present?
= stylesheet_link_tag custom_css_path, media: 'all'
= yield :header_tags
-# These must come after :header_tags to ensure our initial state has been defined.

View File

@ -11,7 +11,7 @@
= link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn'
- else
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn nav-link nav-button'
= link_to t('auth.register'), new_user_registration_path, class: 'webapp-btn nav-link nav-button'
= link_to t('auth.register'), open_registrations? ? new_user_registration_path : 'https://joinmastodon.org/#getting-started', class: 'webapp-btn nav-link nav-button'
.container= yield