diff options
-rw-r--r-- | Gemfile | 2 | ||||
-rw-r--r-- | app/assets/javascripts/components/actions/accounts.jsx | 11 | ||||
-rw-r--r-- | app/assets/javascripts/components/actions/timelines.jsx | 22 | ||||
-rw-r--r-- | app/assets/javascripts/components/components/video_player.jsx | 4 | ||||
-rw-r--r-- | app/assets/javascripts/components/containers/mastodon.jsx | 13 | ||||
-rw-r--r-- | app/assets/javascripts/components/features/community_timeline/index.jsx | 16 | ||||
-rw-r--r-- | app/assets/javascripts/components/features/public_timeline/index.jsx | 16 | ||||
-rw-r--r-- | app/assets/javascripts/components/locales/fr.jsx | 1 | ||||
-rw-r--r-- | app/assets/javascripts/components/reducers/timelines.jsx | 11 | ||||
-rw-r--r-- | app/assets/javascripts/components/selectors/index.jsx | 2 | ||||
-rw-r--r-- | config/initializers/timeout.rb | 4 | ||||
-rw-r--r-- | config/locales/devise.fr.yml | 1 | ||||
-rw-r--r-- | config/locales/doorkeeper.fr.yml | 3 | ||||
-rw-r--r-- | config/locales/fr.yml | 91 | ||||
-rw-r--r-- | config/locales/simple_form.fr.yml | 16 | ||||
-rw-r--r-- | streaming/index.js | 7 |
16 files changed, 205 insertions, 15 deletions
diff --git a/Gemfile b/Gemfile index 440f2e87b..764010d5d 100644 --- a/Gemfile +++ b/Gemfile @@ -50,6 +50,7 @@ gem 'rails-settings-cached' gem 'simple-navigation' gem 'statsd-instrument' gem 'ruby-oembed', require: 'oembed' +gem 'rack-timeout' gem 'react-rails' gem 'browserify-rails' @@ -89,5 +90,4 @@ group :production do gem 'rails_12factor' gem 'redis-rails' gem 'lograge' - gem 'rack-timeout' end diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx index 05fa8e68d..37ebb9969 100644 --- a/app/assets/javascripts/components/actions/accounts.jsx +++ b/app/assets/javascripts/components/actions/accounts.jsx @@ -579,15 +579,18 @@ export function expandFollowingFail(id, error) { }; }; -export function fetchRelationships(account_ids) { +export function fetchRelationships(accountIds) { return (dispatch, getState) => { - if (account_ids.length === 0) { + const loadedRelationships = getState().get('relationships'); + const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null); + + if (newAccountIds.length === 0) { return; } - dispatch(fetchRelationshipsRequest(account_ids)); + dispatch(fetchRelationshipsRequest(newAccountIds)); - api(getState).get(`/api/v1/accounts/relationships?${account_ids.map(id => `id[]=${id}`).join('&')}`).then(response => { + api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { dispatch(fetchRelationshipsSuccess(response.data)); }).catch(error => { dispatch(fetchRelationshipsFail(error)); diff --git a/app/assets/javascripts/components/actions/timelines.jsx b/app/assets/javascripts/components/actions/timelines.jsx index 3e2d4ff43..6cd1f04b3 100644 --- a/app/assets/javascripts/components/actions/timelines.jsx +++ b/app/assets/javascripts/components/actions/timelines.jsx @@ -14,6 +14,9 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; +export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; + export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) { return { type: TIMELINE_REFRESH_SUCCESS, @@ -76,6 +79,11 @@ export function refreshTimeline(timeline, id = null) { let skipLoading = false; if (newestId !== null && getState().getIn(['timelines', timeline, 'loaded']) && (id === null || getState().getIn(['timelines', timeline, 'id']) === id)) { + if (id === null && getState().getIn(['timelines', timeline, 'online'])) { + // Skip refreshing when timeline is live anyway + return; + } + params = { ...params, since_id: newestId }; skipLoading = true; } @@ -162,3 +170,17 @@ export function scrollTopTimeline(timeline, top) { top }; }; + +export function connectTimeline(timeline) { + return { + type: TIMELINE_CONNECT, + timeline + }; +}; + +export function disconnectTimeline(timeline) { + return { + type: TIMELINE_DISCONNECT, + timeline + }; +}; diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx index 1d41c14d4..ab21ca9cd 100644 --- a/app/assets/javascripts/components/components/video_player.jsx +++ b/app/assets/javascripts/components/components/video_player.jsx @@ -24,7 +24,7 @@ const muteStyle = { top: '10px', right: '10px', color: 'white', - boxShadow: '1px 1px 1px #000', + textShadow: "0px 1px 1px black, 1px 0px 1px black", opacity: '0.8', zIndex: '5' }; @@ -57,7 +57,7 @@ const spoilerButtonStyle = { top: '6px', left: '8px', color: 'white', - boxShadow: '1px 1px 1px #000', + textShadow: "0px 1px 1px black, 1px 0px 1px black", zIndex: '100' }; diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 40fbac525..6dc08bb4c 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -4,7 +4,9 @@ import { refreshTimelineSuccess, updateTimeline, deleteFromTimelines, - refreshTimeline + refreshTimeline, + connectTimeline, + disconnectTimeline } from '../actions/timelines'; import { updateNotifications, refreshNotifications } from '../actions/notifications'; import createBrowserHistory from 'history/lib/createBrowserHistory'; @@ -70,6 +72,14 @@ const Mastodon = React.createClass({ this.subscription = createStream(accessToken, 'user', { + connected () { + store.dispatch(connectTimeline('home')); + }, + + disconnected () { + store.dispatch(disconnectTimeline('home')); + }, + received (data) { switch(data.event) { case 'update': @@ -85,6 +95,7 @@ const Mastodon = React.createClass({ }, reconnected () { + store.dispatch(connectTimeline('home')); store.dispatch(refreshTimeline('home')); store.dispatch(refreshNotifications()); } diff --git a/app/assets/javascripts/components/features/community_timeline/index.jsx b/app/assets/javascripts/components/features/community_timeline/index.jsx index 2cfd7b2fe..0957338cf 100644 --- a/app/assets/javascripts/components/features/community_timeline/index.jsx +++ b/app/assets/javascripts/components/features/community_timeline/index.jsx @@ -5,7 +5,9 @@ import Column from '../ui/components/column'; import { refreshTimeline, updateTimeline, - deleteFromTimelines + deleteFromTimelines, + connectTimeline, + disconnectTimeline } from '../../actions/timelines'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnBackButtonSlim from '../../components/column_back_button_slim'; @@ -44,6 +46,18 @@ const CommunityTimeline = React.createClass({ subscription = createStream(accessToken, 'public:local', { + connected () { + dispatch(connectTimeline('community')); + }, + + reconnected () { + dispatch(connectTimeline('community')); + }, + + disconnected () { + dispatch(disconnectTimeline('community')); + }, + received (data) { switch(data.event) { case 'update': diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx index b2342abbd..6d766a83b 100644 --- a/app/assets/javascripts/components/features/public_timeline/index.jsx +++ b/app/assets/javascripts/components/features/public_timeline/index.jsx @@ -5,7 +5,9 @@ import Column from '../ui/components/column'; import { refreshTimeline, updateTimeline, - deleteFromTimelines + deleteFromTimelines, + connectTimeline, + disconnectTimeline } from '../../actions/timelines'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnBackButtonSlim from '../../components/column_back_button_slim'; @@ -44,6 +46,18 @@ const PublicTimeline = React.createClass({ subscription = createStream(accessToken, 'public', { + connected () { + dispatch(connectTimeline('public')); + }, + + reconnected () { + dispatch(connectTimeline('public')); + }, + + disconnected () { + dispatch(disconnectTimeline('public')); + }, + received (data) { switch(data.event) { case 'update': diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx index 2f5dd182f..a45b04b0c 100644 --- a/app/assets/javascripts/components/locales/fr.jsx +++ b/app/assets/javascripts/components/locales/fr.jsx @@ -33,6 +33,7 @@ const fr = { "navigation_bar.logout": "Déconnexion", "navigation_bar.preferences": "Préférences", "navigation_bar.public_timeline": "Fil public", + "navigation_bar.local_timeline": "Fil local", "notification.favourite": "{name} a ajouté à ses favoris :", "notification.follow": "{name} vous suit.", "notification.mention": "{name} vous a mentionné⋅e :", diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx index c67d05423..675a52759 100644 --- a/app/assets/javascripts/components/reducers/timelines.jsx +++ b/app/assets/javascripts/components/reducers/timelines.jsx @@ -7,7 +7,9 @@ import { TIMELINE_EXPAND_SUCCESS, TIMELINE_EXPAND_REQUEST, TIMELINE_EXPAND_FAIL, - TIMELINE_SCROLL_TOP + TIMELINE_SCROLL_TOP, + TIMELINE_CONNECT, + TIMELINE_DISCONNECT } from '../actions/timelines'; import { REBLOG_SUCCESS, @@ -35,6 +37,7 @@ const initialState = Immutable.Map({ path: () => '/api/v1/timelines/home', next: null, isLoading: false, + online: false, loaded: false, top: true, unread: 0, @@ -45,6 +48,7 @@ const initialState = Immutable.Map({ path: () => '/api/v1/timelines/public', next: null, isLoading: false, + online: false, loaded: false, top: true, unread: 0, @@ -56,6 +60,7 @@ const initialState = Immutable.Map({ next: null, params: { local: true }, isLoading: false, + online: false, loaded: false, top: true, unread: 0, @@ -300,6 +305,10 @@ export default function timelines(state = initialState, action) { return filterTimelines(state, action.relationship, action.statuses); case TIMELINE_SCROLL_TOP: return updateTop(state, action.timeline, action.top); + case TIMELINE_CONNECT: + return state.setIn([action.timeline, 'online'], true); + case TIMELINE_DISCONNECT: + return state.setIn([action.timeline, 'online'], false); default: return state; } diff --git a/app/assets/javascripts/components/selectors/index.jsx b/app/assets/javascripts/components/selectors/index.jsx index 0e88654a1..01a6cb264 100644 --- a/app/assets/javascripts/components/selectors/index.jsx +++ b/app/assets/javascripts/components/selectors/index.jsx @@ -5,7 +5,7 @@ const getStatuses = state => state.get('statuses'); const getAccounts = state => state.get('accounts'); const getAccountBase = (state, id) => state.getIn(['accounts', id], null); -const getAccountRelationship = (state, id) => state.getIn(['relationships', id]); +const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null); export const makeGetAccount = () => { return createSelector([getAccountBase, getAccountRelationship], (base, relationship) => { diff --git a/config/initializers/timeout.rb b/config/initializers/timeout.rb index 06a29492e..de87fd906 100644 --- a/config/initializers/timeout.rb +++ b/config/initializers/timeout.rb @@ -1,4 +1,6 @@ +Rack::Timeout::Logger.disable +Rack::Timeout.service_timeout = false + if Rails.env.production? Rack::Timeout.service_timeout = 90 - Rack::Timeout::Logger.disable end diff --git a/config/locales/devise.fr.yml b/config/locales/devise.fr.yml index b64601e7b..ce44d041a 100644 --- a/config/locales/devise.fr.yml +++ b/config/locales/devise.fr.yml @@ -58,3 +58,4 @@ fr: not_locked: n'était pas verrouillé(e) not_saved: one: '1 erreur a empêché ce(tte) %{resource} d''être sauvegardé(e) :' + other: '%{count} erreurs ont empêché ce(tte) %{resource} d''être sauvegardé(e): ' diff --git a/config/locales/doorkeeper.fr.yml b/config/locales/doorkeeper.fr.yml index 6f3c0864a..45fe08e20 100644 --- a/config/locales/doorkeeper.fr.yml +++ b/config/locales/doorkeeper.fr.yml @@ -54,7 +54,7 @@ fr: title: Une erreur est survenue new: able_to: Cette application pourra - prompt: Autorisez %{client_name} à utiliser votre compte? + prompt: Autoriser %{client_name} à utiliser votre compte? title: Autorisation requise show: title: Code d'autorisation @@ -67,6 +67,7 @@ fr: application: Application created_at: Créé le date_format: "%Y-%m-%d %H:%M:%S" + scopes: permissions title: Vos applications autorisées errors: messages: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 173e8d16c..c0bda95b7 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -2,9 +2,30 @@ fr: about: about_mastodon: Mastodon est un serveur <em>libre</em> de réseautage social. Alternative <em>décentralisée</em> aux plateformes commerciales, la monopolisation de vos communications par une entreprise unique est évitée. Tout un chacun peut faire tourner Mastodon et participer au <em>réseau social</em> de manière transparente. + about_this: À propos de cette instance + apps: Applications + business_email: E-mail professionnel + description_headline: Qu'est-ce que %{domain} ? + domain_count_after: autres instances + domain_count_before: Connectés à + features: + api: API ouverte aux apps et services + blocking: Outils complets de bloquage et masquage + characters: 500 caractères par post + chronology: Fil chronologique + ethics: 'Pas de pubs, pas de pistage' + gifv: Partage de vidéos et de GIFs + privacy: Réglages de confidentialité au niveau des posts + public: Fils publics + features_headline: Ce qui rend Mastodon différent get_started: Rejoindre le réseau + links: Liens source_code: Code source + status_count_after: posts + status_count_before: Ayant publiés terms: Conditions d’utilisation + user_count_after: utilisateurs + user_count_before: Abrite accounts: follow: Suivre followers: Abonnés @@ -13,18 +34,33 @@ fr: people_followed_by: Personnes suivies par %{name} people_who_follow: Personnes qui suivent %{name} posts: Statuts + remote_follow: Suivre à distance unfollow: Ne plus suivre application_mailer: + settings: 'Changer les préférences e-mail: ${link}' signature: Notifications de Mastodon depuis %{instance} + view: 'Voir:' + applications: + invalid_url: L'URL fournie est invalide auth: change_password: Changer de mot de passe didnt_get_confirmation: Vous n’avez pas reçu les consignes de confirmation ? forgot_password: Mode passe oublié ? login: Se connecter + logout: Se déconnecter register: S’inscrire resend_confirmation: Envoyer à nouveau les consignes de confirmation reset_password: Réinitialiser le mot de passe - set_new_password: Établir le nouveau mot de passe + set_new_password: Définir le nouveau mot de passe + authorize_follow: + follow: Suivre + prompt_html: 'Vous (<strong>%{self}</strong>) avez demandé à suivre:' + title: Suivre %{acct} + exports: + blocks: Vous bloquez + csv: CSV + follows: Vous suivez + storage: Médias stockés generic: changes_saved_msg: Les modifications ont été enregistrées avec succès ! powered_by: propulsé par %{link} @@ -32,7 +68,24 @@ fr: validation_errors: one: Quelque chose ne va pas ! Vérifiez l’erreur ci-dessous. other: Quelques choses ne vont pas ! Vérifiez les erreurs ci-dessous. + imports: + preface: Vous pouvez importer certaines données comme les personnes que vous suivez ou bloquez sur votre compte sur cette instance à partir de fichiers crées sur une autre instance. + success: Vos données ont été importées avec succès et seront traités en temps et en heure + types: + blocking: Liste d'utilisateurs bloqués + following: Liste d'utilisateurs suivis + upload: Importer + landing_strip_html: <strong>%{name}</strong> utilise <strong>%{domain}</strong>. Vous pouvez le/la suivre et intéragir si vous possédez un compte quelque part dans le "fediverse". Si ce n'est pas le cas, vous pouvez <a href="%{sign_up_path}">en créer un ici</a>. notification_mailer: + digest: + body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}):' + mention: '%{name} vous a mentionné⋅e' + new_followers_summary: + one: Vous avez un nouvel abonné! Youpi! + other: Vous avez %{count} nouveaux abonnés! Incroyable! + subject: + one: "Une nouvelle notification depuis votre dernière visite \U0001F418" + other: "%{count} nouvelles notifications depuis votre dernière visite \U0001F418" favourite: body: "%{name} a ajouté votre statut à ses favoris :" subject: "%{name} a ajouté votre statut à ses favoris" @@ -48,8 +101,44 @@ fr: pagination: next: Suivant prev: Précédent + remote_follow: + acct: Entrez votre pseudo@instance depuis lequel vous voulez suivre ce⋅tte utilisateur⋅trice + missing_resource: L'URL de redirection n'a pas pu être trouvée + proceed: Continuez pour suivre + prompt: 'Vous allez suivre :' settings: + authorized_apps: Applications autorisées + back: Retour vers Mastodon edit_profile: Modifier le profil + export: Export de données + import: Import preferences: Préférences + settings: Réglages + two_factor_auth: Identification à deux facteurs (Two-factor auth) + statuses: + open_in_web: Ouvrir sur le web + over_character_limit: limite de caractères dépassée de %{max} caractères + show_more: Montrer plus + visibilities: + private: Abonnés uniquement + public: Public + unlisted: Public sans être affiché sur le fil public + stream_entries: + click_to_show: Clic pour afficher + reblogged: partagé + sensitive_content: Contenu sensible + time: + formats: + default: '%d %b %Y, %H:%M' + two_factor_auth: + description_html: Si vous activez <strong>l'identification à deux facteurs</strong> vous devrez être en posession de votre téléphone afin de générer un code de connexion. + disable: Désactiver + enable: Activer + instructions_html: "<strong>Scannez ce QR code grâce à Google Authenticator or une application similaire sur votre téléphone</strong>. Désormais, cette application générera des jetons que vous devrez saisir à chaque connexion" + plaintext_secret_html: 'Code secret en clair: <samp>%{secret}</samp>' + warning: Si vous ne pouvez pas configurer une application d'authentification maintenant, vous devriez cliquer sur "Désactiver" pour ne pas bloquer l'accès à votre compte + users: + invalid_email: L'adresse e-mail est invalide + invalid_otp_token: Le code d'identification à deux facteurs est invalide will_paginate: page_gap: "…" diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index 0fcf89140..bcd680722 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -1,26 +1,42 @@ --- fr: simple_form: + hints: + defaults: + avatar: Au format PNG, GIF ou JPG. 2Mo maximum. Sera réduit à 120x120px + display_name: 30 caractères maximum + header: Au format PNG, GIF ou JPG. 2Mo maximum. Sera réduit à 700x335px + locked: Vous devrez approuver chaque abonné⋅e et vos statuts ne s'afficheront qu'à vos abonné⋅es + note: 160 caractères maximum + imports: + data: Un fichier CSV généré par une autre instance de Mastodon labels: defaults: avatar: Image de profil confirm_new_password: Confirmation du nouveau mot de passe confirm_password: Confirmation du mot de passe current_password: Mot de passe actuel + data: Données display_name: Nom public email: Adresse courriel header: Image d’en-tête locale: Langue + locked: Rendre le compte privé new_password: Nouveau mot de passe note: Présentation + otp_attempt: Code d'identification à deux facteurs password: Mot de passe + setting_default_privacy: Confidentialité des statuts + type: Type d'import username: Identifiant interactions: must_be_follower: Masquer les notifications des personnes qui ne vous suivent pas must_be_following: Masquer les notifications des personnes que vous ne suivez pas notification_emails: + digest: Envoyer des emails récapitulatifs favourite: Envoyer un courriel lorsque quelqu’un ajoute mes statut à ses favoris follow: Envoyer un courriel lorsque quelqu’un me suit + follow_request: Envoyer un courriel lorsque quelqu'un demande à me suivre mention: Envoyer un courriel lorsque quelqu’un me mentionne reblog: Envoyer un courriel lorsque quelqu’un partage mes statuts 'no': Non diff --git a/streaming/index.js b/streaming/index.js index 0f838e411..7edf6203f 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -215,8 +215,11 @@ const streamHttpEnd = req => (id, listener) => { // Setup stream output to WebSockets const streamToWs = (req, ws) => { + const heartbeat = setInterval(() => ws.ping(), 15000) + ws.on('close', () => { log.verbose(req.requestId, `Ending stream for ${req.accountId}`) + clearInterval(heartbeat) }) return (event, payload) => { @@ -234,6 +237,10 @@ const streamWsEnd = ws => (id, listener) => { ws.on('close', () => { unsubscribe(id, listener) }) + + ws.on('error', e => { + unsubscribe(id, listener) + }) } app.use(setRequestId) |