Merge branch 'master' into patch-1

This commit is contained in:
Effy Elden 2017-04-08 21:58:35 +10:00 committed by GitHub
commit 03fe20acf0
41 changed files with 247 additions and 85 deletions

View File

@ -17,7 +17,7 @@ Click on the screenshot to watch a demo of the UI:
[youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU [youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU
Focus of the project on a clean REST API and a good user interface. Ruby on Rails is used for the back-end, while React.js and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided. The project focus is a clean REST API and a good user interface. Ruby on Rails is used for the back-end, while React.js and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
If you would like, you can [support the development of this project on Patreon][patreon]. Alternatively, you can donate to this BTC address: `17j2g7vpgHhLuXhN4bueZFCvdxxieyRVWd` If you would like, you can [support the development of this project on Patreon][patreon]. Alternatively, you can donate to this BTC address: `17j2g7vpgHhLuXhN4bueZFCvdxxieyRVWd`

View File

@ -26,6 +26,10 @@
"description": "The secret key base", "description": "The secret key base",
"generator": "secret" "generator": "secret"
}, },
"OTP_SECRET": {
"description": "One-time password secret",
"generator": "secret"
},
"SINGLE_USER_MODE": { "SINGLE_USER_MODE": {
"description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)", "description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)",
"value": "false", "value": "false",

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" viewBox="0 0 1000 1000">
<path d="M527.194 543.7a28.362 28.362 0 0 0-56.723 0 25.73 25.73 0 0 0 2.67 11.674 26.42 26.42 0 0 0 5.672 8.34 28.2 28.2 0 0 0 40.04 0 31.87 31.87 0 0 0 6.006-8.34 28.8 28.8 0 0 0 2.336-11.674m-48.382-113.413a28.308 28.308 0 1 0 40.04 40.027 37.2 37.2 0 0 0 4.67-5.67 28.092 28.092 0 0 0 3.67-14.343 27.29 27.29 0 0 0-8.34-20.012 28.24 28.24 0 0 0-5.006-4 26.958 26.958 0 0 0-15.015-4.336 27.31 27.31 0 0 0-20.02 8.34m20.02-101.735a28.476 28.476 0 1 0 20.02 8.34 27.31 27.31 0 0 0-20.02-8.34M231.9 573.717a28.18 28.18 0 1 0 8.342 20.012 27.308 27.308 0 0 0-8.342-20.014m-40.04-93.4a28.352 28.352 0 0 0 20.02 48.366 26.958 26.958 0 0 0 15.015-4.336 28.255 28.255 0 0 0 5.005-4 27.29 27.29 0 0 0 8.342-20.013 28.09 28.09 0 0 0-3.67-14.343 37.21 37.21 0 0 0-4.67-5.67 28.2 28.2 0 0 0-40.04 0m40.04-93.4a28.2 28.2 0 0 0-40.04 0 26.425 26.425 0 0 0-5.673 8.34 25.73 25.73 0 0 0-2.67 11.673 28.315 28.315 0 0 0 48.38 20.018 27.29 27.29 0 0 0 8.342-20.012 28.8 28.8 0 0 0-2.336-11.674 31.87 31.87 0 0 0-6.006-8.34m550.55 178.453a28.476 28.476 0 1 0 20.02 8.34 27.31 27.31 0 0 0-20.02-8.34m20.02-85.057a28.2 28.2 0 0 0-40.04 0 37.2 37.2 0 0 0-4.672 5.67 28.092 28.092 0 0 0-3.67 14.343 27.29 27.29 0 0 0 8.342 20.013 28.248 28.248 0 0 0 5.005 4 26.96 26.96 0 0 0 15.015 4.336 28.3 28.3 0 0 0 20.02-48.366m-46.046-85.057a28.8 28.8 0 0 0-2.336 11.673 28.362 28.362 0 0 0 56.723 0 25.73 25.73 0 0 0-2.668-11.674 26.427 26.427 0 0 0-5.672-8.34 28.2 28.2 0 0 0-40.04 0 31.86 31.86 0 0 0-6.007 8.343z" fill="#2b90d9"/>
<path d="M853.52 146.764Q707.04 0 499.833 0 292.96 0 146.48 146.764 0 293.2 0 500q0 207.138 146.48 353.57T499.833 1000q207.207 0 353.687-146.43T1000 500q0-206.8-146.48-353.236zM213.547 708.806h-3.337q-43.043 0-73.407-30.02-30.03-30.354-30.03-73.382V395.93v-.666q1.335-41.027 30.03-69.713 30.364-30.35 73.407-30.35t73.073 30.354q29.363 29.02 30.364 70.38V615.41q2.336 55.037 46.713 93.4zM600.6 554.7q-1 41.36-30.364 70.38-30.03 30.353-73.073 30.354t-73.407-30.354q-28.7-28.686-30.03-69.713V345.23q0-43.03 30.03-73.382 30.364-30.02 73.407-30.02h150.15q-44.378 38.36-46.713 93.4zm286.954 50.7q0 43.03-30.03 73.382-30.364 30.02-73.407 30.02h-150.15q44.378-38.36 46.713-93.4v-219.47q1-41.362 30.364-70.38 30.03-30.355 73.073-30.355t73.407 30.354q28.7 28.687 30.03 69.714V605.4z" fill="#2b90d9"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -15,6 +15,7 @@ const ColumnCollapsable = React.createClass({
propTypes: { propTypes: {
icon: React.PropTypes.string.isRequired, icon: React.PropTypes.string.isRequired,
title: React.PropTypes.string,
fullHeight: React.PropTypes.number.isRequired, fullHeight: React.PropTypes.number.isRequired,
children: React.PropTypes.node, children: React.PropTypes.node,
onCollapse: React.PropTypes.func onCollapse: React.PropTypes.func
@ -39,13 +40,13 @@ const ColumnCollapsable = React.createClass({
}, },
render () { render () {
const { icon, fullHeight, children } = this.props; const { icon, title, fullHeight, children } = this.props;
const { collapsed } = this.state; const { collapsed } = this.state;
const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable'; const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable';
return ( return (
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<div style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div> <div title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}> <Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
{({ opacity, height }) => {({ opacity, height }) =>

View File

@ -6,7 +6,8 @@ import SettingToggle from '../../notifications/components/setting_toggle';
import SettingText from './setting_text'; import SettingText from './setting_text';
const messages = defineMessages({ const messages = defineMessages({
filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' } filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
settings: { id: 'home.settings', defaultMessage: 'Column settings' }
}); });
const outerStyle = { const outerStyle = {
@ -39,7 +40,7 @@ const ColumnSettings = React.createClass({
const { settings, onChange, onSave, intl } = this.props; const { settings, onChange, onSave, intl } = this.props;
return ( return (
<ColumnCollapsable icon='sliders' fullHeight={209} onCollapse={onSave}> <ColumnCollapsable icon='sliders' title={intl.formatMessage(messages.settings)} fullHeight={209} onCollapse={onSave}>
<div className='column-settings--outer' style={outerStyle}> <div className='column-settings--outer' style={outerStyle}>
<span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span> <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>

View File

@ -1,3 +1,9 @@
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
clear: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }
});
const iconStyle = { const iconStyle = {
fontSize: '16px', fontSize: '16px',
padding: '15px', padding: '15px',
@ -8,14 +14,22 @@ const iconStyle = {
zIndex: '2' zIndex: '2'
}; };
const ClearColumnButton = ({ onClick }) => ( const ClearColumnButton = React.createClass({
<div className='column-icon' tabindex='0' style={iconStyle} onClick={onClick}>
<i className='fa fa-trash' />
</div>
);
ClearColumnButton.propTypes = { propTypes: {
onClick: React.PropTypes.func.isRequired onClick: React.PropTypes.func.isRequired,
}; intl: React.PropTypes.object.isRequired
},
export default ClearColumnButton; render () {
const { intl } = this.props;
return (
<div title={intl.formatMessage(messages.clear)} className='column-icon' tabIndex='0' style={iconStyle} onClick={this.onClick}>
<i className='fa fa-eraser' />
</div>
);
}
})
export default injectIntl(ClearColumnButton);

View File

@ -1,9 +1,13 @@
import PureRenderMixin from 'react-addons-pure-render-mixin'; import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnCollapsable from '../../../components/column_collapsable'; import ColumnCollapsable from '../../../components/column_collapsable';
import SettingToggle from './setting_toggle'; import SettingToggle from './setting_toggle';
const messages = defineMessages({
settings: { id: 'notifications.settings', defaultMessage: 'Column settings' }
});
const outerStyle = { const outerStyle = {
padding: '15px' padding: '15px'
}; };
@ -30,14 +34,14 @@ const ColumnSettings = React.createClass({
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
render () { render () {
const { settings, onChange, onSave } = this.props; const { settings, intl, onChange, onSave } = this.props;
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />; const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />; const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />; const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
return ( return (
<ColumnCollapsable icon='sliders' fullHeight={616} onCollapse={onSave}> <ColumnCollapsable icon='sliders' title={intl.formatMessage(messages.settings)} fullHeight={616} onCollapse={onSave}>
<div className='column-settings--outer' style={outerStyle}> <div className='column-settings--outer' style={outerStyle}>
<span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span> <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
@ -77,4 +81,4 @@ const ColumnSettings = React.createClass({
}); });
export default ColumnSettings; export default injectIntl(ColumnSettings);

View File

@ -1,15 +1,15 @@
const en = { const en = {
"column_back_button.label": "Zurück", "column_back_button.label": "Zurück",
"lightbox.close": "Schließen", "lightbox.close": "Schließen",
"loading_indicator.label": "Lade...", "loading_indicator.label": "Lade",
"status.mention": "Erwähnen", "status.mention": "Erwähnen",
"status.delete": "Löschen", "status.delete": "Löschen",
"status.reply": "Antworten", "status.reply": "Antworten",
"status.reblog": "Teilen", "status.reblog": "Teilen",
"status.favourite": "Favorisieren", "status.favourite": "Favorisieren",
"status.reblogged_by": "{name} teilte", "status.reblogged_by": "{name} teilte",
"status.sensitive_warning": "Sensible Inhalte", "status.sensitive_warning": "Heikle Inhalte",
"status.sensitive_toggle": "Klicken um zu zeigen", "status.sensitive_toggle": "Klicke, um sie zu sehen",
"status.open": "Öffnen", "status.open": "Öffnen",
"video_player.toggle_sound": "Ton umschalten", "video_player.toggle_sound": "Ton umschalten",
"account.mention": "Erwähnen", "account.mention": "Erwähnen",
@ -20,17 +20,17 @@ const en = {
"account.follow": "Folgen", "account.follow": "Folgen",
"account.posts": "Beiträge", "account.posts": "Beiträge",
"account.follows": "Folgt", "account.follows": "Folgt",
"account.followers": "Folger", "account.followers": "Folgende",
"account.follows_you": "Folgt dir", "account.follows_you": "Folgt dir",
"account.requested": "Warte auf Erlaubnis", "account.requested": "Warte auf Erlaubnis",
"getting_started.heading": "Erste Schritte", "getting_started.heading": "Erste Schritte",
"getting_started.about_addressing": "Du kannst Leuten folgen, falls du ihren Nutzernamen und ihre Domain kennst, in dem du eine e-mail-artige Addresse in das Suchfeld oben an der Seite eingibst.", "getting_started.about_addressing": "Du kannst Leuten folgen, falls du ihren Nutzernamen und ihre Domain kennst, in dem du eine e-mail-artige Addresse in das Suchfeld oben auf der Seite eingibst.",
"getting_started.about_shortcuts": "Falls der Zielnutzer an derselben Domain ist wie du, funktioniert der Nutzername auch alleine. Das gilt auch für Erwähnungen in Beiträgen.", "getting_started.about_shortcuts": "Falls die Person auf derselben Domain ist wie du, reicht auch ihr Nutzername alleine. Das gilt auch für Erwähnungen in Beiträgen.",
"getting_started.about_developer": "Der Entwickler des Projekts kann unter Gargron@mastodon.social gefunden werden", "getting_started.about_developer": "Der Entwickler des Projekts kann unter Gargron@mastodon.social gefunden werden",
"getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.", "getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
"column.home": "Home", "column.home": "Home",
"column.mentions": "Erwähnungen", "column.mentions": "Erwähnungen",
"column.public": "Gesamtes Bekanntes Netz", "column.public": "Gesamtes bekanntes Netz",
"column.notifications": "Mitteilungen", "column.notifications": "Mitteilungen",
"column.follow_requests": "Folgeanfragen", "column.follow_requests": "Folgeanfragen",
"tabs_bar.compose": "Schreiben", "tabs_bar.compose": "Schreiben",
@ -38,11 +38,11 @@ const en = {
"tabs_bar.mentions": "Erwähnungen", "tabs_bar.mentions": "Erwähnungen",
"tabs_bar.public": "Gesamtes Netz", "tabs_bar.public": "Gesamtes Netz",
"tabs_bar.notifications": "Mitteilungen", "tabs_bar.notifications": "Mitteilungen",
"compose_form.placeholder": "Worüber möchstest du schreiben?", "compose_form.placeholder": "Worüber möchtest du schreiben?",
"compose_form.publish": "Tröt", "compose_form.publish": "Tröt",
"compose_form.sensitive": "Medien als sensitiv markieren", "compose_form.sensitive": "Medien als heikel markieren",
"compose_form.unlisted": "Öffentlich nicht auflisten",
"compose_form.private": "Als privat markieren", "compose_form.private": "Als privat markieren",
"compose_form.unlisted": "Nicht öffentlich auflisten",
"navigation_bar.edit_profile": "Profil bearbeiten", "navigation_bar.edit_profile": "Profil bearbeiten",
"navigation_bar.preferences": "Einstellungen", "navigation_bar.preferences": "Einstellungen",
"navigation_bar.public_timeline": "Öffentlich", "navigation_bar.public_timeline": "Öffentlich",
@ -52,15 +52,15 @@ const en = {
"search.placeholder": "Suche", "search.placeholder": "Suche",
"search.account": "Konto", "search.account": "Konto",
"search.hashtag": "Hashtag", "search.hashtag": "Hashtag",
"upload_button.label": "Media-Datei anfügen", "upload_button.label": "Mediendatei hinzufügen",
"upload_form.undo": "Entfernen", "upload_form.undo": "Entfernen",
"notification.follow": "{name} folgt dir", "notification.follow": "{name} folgt dir",
"notification.favourite": "{name} favorisierte deinen Status", "notification.favourite": "{name} favorisierte deinen Status",
"notification.reblog": "{name} teilte deinen Status", "notification.reblog": "{name} teilte deinen Status",
"notification.mention": "{name} erwähnte dich", "notification.mention": "{name} erwähnte dich",
"notifications.column_settings.alert": "Desktop-Benachrichtigunen", "notifications.column_settings.alert": "Desktop-Benachrichtigungen",
"notifications.column_settings.show": "In der Spalte anzeigen", "notifications.column_settings.show": "In der Spalte anzeigen",
"notifications.column_settings.follow": "Neue Folger:", "notifications.column_settings.follow": "Neue Folgende:",
"notifications.column_settings.favourite": "Favorisierungen:", "notifications.column_settings.favourite": "Favorisierungen:",
"notifications.column_settings.mention": "Erwähnungen:", "notifications.column_settings.mention": "Erwähnungen:",
"notifications.column_settings.reblog": "Geteilte Beiträge:", "notifications.column_settings.reblog": "Geteilte Beiträge:",

View File

@ -10,6 +10,10 @@ const en = {
"status.reblogged_by": "{name} boosted", "status.reblogged_by": "{name} boosted",
"status.sensitive_warning": "Sensitive content", "status.sensitive_warning": "Sensitive content",
"status.sensitive_toggle": "Click to view", "status.sensitive_toggle": "Click to view",
"status.show_more": "Show more",
"status.show_less": "Show less",
"status.open": "Expand this status",
"status.report": "Report @{name}",
"video_player.toggle_sound": "Toggle sound", "video_player.toggle_sound": "Toggle sound",
"account.mention": "Mention @{name}", "account.mention": "Mention @{name}",
"account.edit_profile": "Edit profile", "account.edit_profile": "Edit profile",

View File

@ -10,6 +10,10 @@ const fr = {
"status.reblogged_by": "{name} a partagé :", "status.reblogged_by": "{name} a partagé :",
"status.sensitive_warning": "Contenu délicat", "status.sensitive_warning": "Contenu délicat",
"status.sensitive_toggle": "Cliquer pour dévoiler", "status.sensitive_toggle": "Cliquer pour dévoiler",
"status.show_more": "Déplier",
"status.show_less": "Replier",
"status.open": "Déplier ce status",
"status.report": "Signaler @{name}",
"video_player.toggle_sound": "Mettre/Couper le son", "video_player.toggle_sound": "Mettre/Couper le son",
"account.mention": "Mentionner", "account.mention": "Mentionner",
"account.edit_profile": "Modifier le profil", "account.edit_profile": "Modifier le profil",
@ -35,7 +39,6 @@ const fr = {
"column.community": "Fil public local", "column.community": "Fil public local",
"column.public": "Fil public global", "column.public": "Fil public global",
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.public": "Fil public",
"column.blocks": "Utilisateurs bloqués", "column.blocks": "Utilisateurs bloqués",
"column.favourites": "Favoris", "column.favourites": "Favoris",
"tabs_bar.compose": "Composer", "tabs_bar.compose": "Composer",
@ -44,9 +47,9 @@ const fr = {
"tabs_bar.public": "Fil public global", "tabs_bar.public": "Fil public global",
"tabs_bar.notifications": "Notifications", "tabs_bar.notifications": "Notifications",
"compose_form.placeholder": "Quavez-vous en tête ?", "compose_form.placeholder": "Quavez-vous en tête ?",
"compose_form.publish": "Pouet ", "compose_form.publish": "Pouet",
"compose_form.sensitive": "Marquer le média comme délicat", "compose_form.sensitive": "Marquer le média comme délicat",
"compose_form.spoiler": "Masquer le texte par un avertissement", "compose_form.spoiler": "Masquer le texte derrière un avertissement",
"compose_form.private": "Rendre privé", "compose_form.private": "Rendre privé",
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodons. Si {domains} {domainsCount, plural, one {n'est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n'y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d'une autre manière à d'autres personnes imprévues", "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodons. Si {domains} {domainsCount, plural, one {n'est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n'y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d'une autre manière à d'autres personnes imprévues",
"compose_form.unlisted": "Ne pas afficher dans les fils publics", "compose_form.unlisted": "Ne pas afficher dans les fils publics",
@ -58,7 +61,6 @@ const fr = {
"navigation_bar.blocks": "Utilisateurs bloqués", "navigation_bar.blocks": "Utilisateurs bloqués",
"navigation_bar.favourites": "Favoris", "navigation_bar.favourites": "Favoris",
"navigation_bar.info": "Plus d'informations", "navigation_bar.info": "Plus d'informations",
"notification.favourite": "{name} a ajouté à ses favoris :",
"navigation_bar.logout": "Déconnexion", "navigation_bar.logout": "Déconnexion",
"reply_indicator.cancel": "Annuler", "reply_indicator.cancel": "Annuler",
"search.placeholder": "Chercher", "search.placeholder": "Chercher",

View File

@ -1,14 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include Localized
# Prevent CSRF attacks by raising an exception. # Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead. # For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception protect_from_forgery with: :exception
force_ssl if: "Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'" force_ssl if: "Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'"
include Localized
helper_method :current_account helper_method :current_account
rescue_from ActionController::RoutingError, with: :not_found rescue_from ActionController::RoutingError, with: :not_found
@ -41,7 +40,6 @@ class ApplicationController < ActionController::Base
# If the sign in is after a two week break, we need to regenerate their feed # If the sign in is after a two week break, we need to regenerate their feed
RegenerationWorker.perform_async(current_user.account_id) if current_user.last_sign_in_at < 14.days.ago RegenerationWorker.perform_async(current_user.account_id) if current_user.last_sign_in_at < 14.days.ago
return
end end
def check_suspension def check_suspension

View File

@ -4,13 +4,25 @@ module Localized
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
before_action :set_locale around_action :set_locale
end end
private
def set_locale def set_locale
I18n.locale = current_user.try(:locale) || default_locale locale = default_locale
rescue I18n::InvalidLocale
I18n.locale = default_locale if user_signed_in?
begin
locale = current_user.try(:locale) || default_locale
rescue I18n::InvalidLocale
locale = default_locale
end
end
I18n.with_locale(locale) do
yield
end
end end
def default_locale def default_locale

View File

@ -1,13 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
include Localized
skip_before_action :authenticate_resource_owner! skip_before_action :authenticate_resource_owner!
before_action :store_current_location before_action :store_current_location
before_action :authenticate_resource_owner! before_action :authenticate_resource_owner!
include Localized
private private
def store_current_location def store_current_location

View File

@ -1,13 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
include Localized
skip_before_action :authenticate_resource_owner! skip_before_action :authenticate_resource_owner!
before_action :store_current_location before_action :store_current_location
before_action :authenticate_resource_owner! before_action :authenticate_resource_owner!
include Localized
private private
def store_current_location def store_current_location

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
module SiteTitleHelper
def site_title
Setting.site_title.to_s
end
end

View File

@ -20,8 +20,6 @@ class FollowRemoteAccountService < BaseService
Rails.logger.debug "Looking up webfinger for #{uri}" Rails.logger.debug "Looking up webfinger for #{uri}"
account = Account.new(username: username, domain: domain)
data = Goldfinger.finger("acct:#{uri}") data = Goldfinger.finger("acct:#{uri}")
raise Goldfinger::Error, 'Missing resource links' if data.link('http://schemas.google.com/g/2010#updates-from').nil? || data.link('salmon').nil? || data.link('http://webfinger.net/rel/profile-page').nil? || data.link('magic-public-key').nil? raise Goldfinger::Error, 'Missing resource links' if data.link('http://schemas.google.com/g/2010#updates-from').nil? || data.link('salmon').nil? || data.link('http://webfinger.net/rel/profile-page').nil? || data.link('magic-public-key').nil?
@ -37,6 +35,7 @@ class FollowRemoteAccountService < BaseService
domain_block = DomainBlock.find_by(domain: domain) domain_block = DomainBlock.find_by(domain: domain)
account = Account.new(username: confirmed_username, domain: confirmed_domain)
account.remote_url = data.link('http://schemas.google.com/g/2010#updates-from').href account.remote_url = data.link('http://schemas.google.com/g/2010#updates-from').href
account.salmon_url = data.link('salmon').href account.salmon_url = data.link('salmon').href
account.url = data.link('http://webfinger.net/rel/profile-page').href account.url = data.link('http://webfinger.net/rel/profile-page').href
@ -51,8 +50,8 @@ class FollowRemoteAccountService < BaseService
account.uri = get_account_uri(xml) account.uri = get_account_uri(xml)
account.hub_url = hubs.first.attribute('href').value account.hub_url = hubs.first.attribute('href').value
get_profile(body, account)
account.save! account.save!
get_profile(body, account)
account account
end end

View File

@ -5,14 +5,13 @@ class ProcessFeedService < BaseService
xml = Nokogiri::XML(body) xml = Nokogiri::XML(body)
xml.encoding = 'utf-8' xml.encoding = 'utf-8'
update_author(body, xml, account) update_author(body, account)
process_entries(xml, account) process_entries(xml, account)
end end
private private
def update_author(body, xml, account) def update_author(body, account)
return if xml.at_xpath('/xmlns:feed', xmlns: TagManager::XMLNS).nil?
RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), true) RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), true)
end end

View File

@ -1,7 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class UpdateRemoteProfileService < BaseService class UpdateRemoteProfileService < BaseService
def call(xml, account, resubscribe = false) def call(body, account, resubscribe = false)
xml = Nokogiri::XML(body)
xml.encoding = 'utf-8'
xml = xml.at_xpath('/xmlns:feed', xmlns: TagManager::XMLNS) || xml.at_xpath('/xmlns:entry', xmlns: TagManager::XMLNS)
return if xml.nil? return if xml.nil?
author_xml = xml.at_xpath('./xmlns:author', xmlns: TagManager::XMLNS) || xml.at_xpath('./dfrn:owner', dfrn: TagManager::DFRN_XMLNS) author_xml = xml.at_xpath('./xmlns:author', xmlns: TagManager::XMLNS) || xml.at_xpath('./dfrn:owner', dfrn: TagManager::DFRN_XMLNS)
@ -12,9 +17,9 @@ class UpdateRemoteProfileService < BaseService
account.note = author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).content unless author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).nil? account.note = author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).content unless author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).nil?
account.locked = author_xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content == 'private' account.locked = author_xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content == 'private'
unless account.suspended? || DomainBlock.find_by(domain: account.domain)&.reject_media? if !account.suspended? && !DomainBlock.find_by(domain: account.domain)&.reject_media?
account.avatar_remote_url = author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'] unless author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS).nil? || author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'].blank? account.avatar_remote_url = link_href_from_xml(author_xml, 'avatar') if link_has_href?(author_xml, 'avatar')
account.header_remote_url = author_xml.at_xpath('./xmlns:link[@rel="header"]', xmlns: TagManager::XMLNS)['href'] unless author_xml.at_xpath('./xmlns:link[@rel="header"]', xmlns: TagManager::XMLNS).nil? || author_xml.at_xpath('./xmlns:link[@rel="header"]', xmlns: TagManager::XMLNS)['href'].blank? account.header_remote_url = link_href_from_xml(author_xml, 'header') if link_has_href?(author_xml, 'header')
end end
end end
@ -25,4 +30,14 @@ class UpdateRemoteProfileService < BaseService
SubscribeService.new.call(account) if resubscribe && (account.hub_url != old_hub_url) SubscribeService.new.call(account) if resubscribe && (account.hub_url != old_hub_url)
end end
private
def link_href_from_xml(xml, type)
xml.at_xpath('./xmlns:link[@rel="' + type + '"]', xmlns: TagManager::XMLNS)['href']
end
def link_has_href?(xml, type)
!(xml.at_xpath('./xmlns:link[@rel="' + type + '"]', xmlns: TagManager::XMLNS).nil? || xml.at_xpath('./xmlns:link[@rel="' + type + '"]', xmlns: TagManager::XMLNS)['href'].blank?)
end
end end

View File

@ -5,7 +5,7 @@
= Rails.configuration.x.local_domain = Rails.configuration.x.local_domain
- content_for :header_tags do - content_for :header_tags do
%meta{ property: 'og:site_name', content: 'Mastodon' }/ %meta{ property: 'og:site_name', content: site_title }/
%meta{ property: 'og:type', content: 'website' }/ %meta{ property: 'og:type', content: 'website' }/
%meta{ property: 'og:title', content: Rails.configuration.x.local_domain }/ %meta{ property: 'og:title', content: Rails.configuration.x.local_domain }/
%meta{ property: 'og:description', content: @description.blank? ? "Mastodon is a free, open-source social network server. A decentralized alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly" : strip_tags(@description) }/ %meta{ property: 'og:description', content: @description.blank? ? "Mastodon is a free, open-source social network server. A decentralized alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly" : strip_tags(@description) }/

View File

@ -5,7 +5,7 @@
%link{ rel: 'salmon', href: api_salmon_url(@account.id) }/ %link{ rel: 'salmon', href: api_salmon_url(@account.id) }/
%link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/ %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/
%meta{ property: 'og:site_name', content: 'Mastodon' }/ %meta{ property: 'og:site_name', content: site_title }/
%meta{ property: 'og:type', content: 'profile' }/ %meta{ property: 'og:type', content: 'profile' }/
%meta{ property: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/ %meta{ property: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/
%meta{ property: 'og:description', content: @account.note }/ %meta{ property: 'og:description', content: @account.note }/

View File

@ -15,7 +15,7 @@
- if content_for?(:page_title) - if content_for?(:page_title)
= yield(:page_title) = yield(:page_title)
= ' - ' = ' - '
= Setting.site_title = site_title
= stylesheet_link_tag 'application', media: 'all' = stylesheet_link_tag 'application', media: 'all'
= csrf_meta_tags = csrf_meta_tags

View File

@ -2,7 +2,7 @@
%link{ rel: 'alternate', type: 'application/atom+xml', href: account_stream_entry_url(@account, @stream_entry, format: 'atom') }/ %link{ rel: 'alternate', type: 'application/atom+xml', href: account_stream_entry_url(@account, @stream_entry, format: 'atom') }/
%link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: account_stream_entry_url(@account, @stream_entry), format: 'json') }/ %link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: account_stream_entry_url(@account, @stream_entry), format: 'json') }/
%meta{ property: 'og:site_name', content: 'Mastodon' }/ %meta{ property: 'og:site_name', content: site_title }/
%meta{ property: 'og:type', content: 'article' }/ %meta{ property: 'og:type', content: 'article' }/
%meta{ property: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/ %meta{ property: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/

View File

@ -0,0 +1,5 @@
<p>Bienvenue <%= @resource.email %>&nbsp;!</p>
<p>Vous pouvez confirmer l'email de votre compte Mastodon en cliquant sur le lien ci-dessous&nbsp;:</p>
<p><%= link_to 'Confirmer mon compte', confirmation_url(@resource, confirmation_token: @token) %></p>

View File

@ -0,0 +1,5 @@
Bienvenue <%= @resource.email %> !
Vous pouvez confirmer l'email de votre compte Mastodon en cliquant sur le lien ci-dessous :
<%= confirmation_url(@resource, confirmation_token: @token) %>

View File

@ -0,0 +1,3 @@
<p>Bonjour <%= @resource.email %>&nbsp;!</p>
<p>Nous vous contactons pour vous informer que votre mot de passe sur Mastodon a bien été modifié.</p>

View File

@ -0,0 +1,3 @@
Bonjour <%= @resource.email %> !
Nous vous contactons pour vous informer que votre mot de passe sur Mastodon a bien été modifié.

View File

@ -0,0 +1,8 @@
<p>Bonjour <%= @resource.email %>&nbsp;!</p>
<p>Quelqu'un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous.</p>
<p><%= link_to 'Modifier mon mot de passe', edit_password_url(@resource, reset_password_token: @token) %></p>
<p>Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message.</p>
<p>Votre mot de passe ne sera pas modifié tant que vous n'accéderez pas au lien ci-dessus et n'en choisirez pas un nouveau.</p>

View File

@ -0,0 +1,8 @@
Bonjour <%= @resource.email %> !
Quelqu'un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous.
<%= edit_password_url(@resource, reset_password_token: @token) %>
Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message.
Votre mot de passe ne sera pas modifié tant que vous n'accéderez pas au lien ci-dessus et n'en choisirez pas un nouveau.

View File

@ -6,14 +6,7 @@ class RemoteProfileUpdateWorker
sidekiq_options queue: 'pull' sidekiq_options queue: 'pull'
def perform(account_id, body, resubscribe) def perform(account_id, body, resubscribe)
account = Account.find(account_id) UpdateRemoteProfileService.new.call(body, Account.find(account_id), resubscribe)
xml = Nokogiri::XML(body)
xml.encoding = 'utf-8'
author_container = xml.at_xpath('/xmlns:feed', xmlns: TagManager::XMLNS) || xml.at_xpath('/xmlns:entry', xmlns: TagManager::XMLNS)
UpdateRemoteProfileService.new.call(author_container, account, resubscribe)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
true true
end end

View File

@ -7,8 +7,8 @@ de:
terms: AGB terms: AGB
accounts: accounts:
follow: Folgen follow: Folgen
followers: Follower followers: Folger
following: Gefolgt following: Folgt
nothing_here: Hier gibt es nichts! nothing_here: Hier gibt es nichts!
people_followed_by: Nutzer, denen %{name} folgt people_followed_by: Nutzer, denen %{name} folgt
people_who_follow: Nutzer, die %{name} folgen people_who_follow: Nutzer, die %{name} folgen

View File

@ -7,7 +7,7 @@ fi:
business_email: 'Business e-mail:' business_email: 'Business e-mail:'
contact: Ota yhteyttä contact: Ota yhteyttä
description_headline: Mikä on %{domain}? description_headline: Mikä on %{domain}?
domain_count_after: muut palvelimet domain_count_after: muuhun palvelimeen
domain_count_before: Yhdistyneenä domain_count_before: Yhdistyneenä
features: features:
api: Avoin API ohjelmille ja palveluille api: Avoin API ohjelmille ja palveluille

View File

@ -5,13 +5,14 @@ fr:
about_this: À propos de cette instance about_this: À propos de cette instance
apps: Applications apps: Applications
business_email: E-mail professionnel business_email: E-mail professionnel
closed_registrations: Les inscriptions sont actuellement fermées sur cette instance. . closed_registrations: Les inscriptions sont actuellement fermées sur cette instance.
contact: Contact
description_headline: Qu'est-ce que %{domain} ? description_headline: Qu'est-ce que %{domain} ?
domain_count_after: autres instances domain_count_after: autres instances
domain_count_before: Connectés à domain_count_before: Connectés à
features: features:
api: API ouverte aux apps et services api: API ouverte aux apps et services
blocking: Outils complets de bloquage et masquage blocks: Outils complets de bloquage et masquage
characters: 500 caractères par post characters: 500 caractères par post
chronology: Fil chronologique chronology: Fil chronologique
ethics: 'Pas de pubs, pas de pistage' ethics: 'Pas de pubs, pas de pistage'
@ -21,6 +22,7 @@ fr:
features_headline: Ce qui rend Mastodon différent features_headline: Ce qui rend Mastodon différent
get_started: Rejoindre le réseau get_started: Rejoindre le réseau
links: Liens links: Liens
other_instances: Autres instances
source_code: Code source source_code: Code source
status_count_after: posts status_count_after: posts
status_count_before: Ayant publié status_count_before: Ayant publié
@ -54,9 +56,24 @@ fr:
reset_password: Réinitialiser le mot de passe reset_password: Réinitialiser le mot de passe
set_new_password: Définir le nouveau mot de passe set_new_password: Définir le nouveau mot de passe
authorize_follow: authorize_follow:
error: Malheureusement, il y a eu une erreur en cherchant les détails du compte distant
follow: Suivre follow: Suivre
prompt_html: 'Vous (<strong>%{self}</strong>) avez demandé à suivre:' prompt_html: 'Vous (<strong>%{self}</strong>) avez demandé à suivre:'
title: Suivre %{acct} title: Suivre %{acct}
datetime:
distance_in_words:
about_x_hours: "%{count}h"
about_x_months: "%{count}mo"
about_x_years: "%{count}y"
almost_x_years: "%{count}y"
half_a_minute: A l'instant
less_than_x_minutes: "%{count}m"
less_than_x_seconds: A l'instant
over_x_years: "%{count}y"
x_days: "%{count}d"
x_minutes: "%{count}m"
x_months: "%{count}mo"
x_seconds: "%{count}s"
exports: exports:
blocks: Vous bloquez blocks: Vous bloquez
csv: CSV csv: CSV
@ -93,6 +110,9 @@ fr:
follow: follow:
body: "%{name} vous suit !" body: "%{name} vous suit !"
subject: "%{name} vous suit" subject: "%{name} vous suit"
follow_request:
body: "%{name} a demandé à vous suivre"
subject: 'Abonné⋅es en attente : %{name}'
mention: mention:
body: "%{name} vous a mentionné⋅e dans :" body: "%{name} vous a mentionné⋅e dans :"
subject: "%{name} vous a mentionné⋅e" subject: "%{name} vous a mentionné⋅e"
@ -132,7 +152,7 @@ fr:
formats: formats:
default: '%d %b %Y, %H:%M' default: '%d %b %Y, %H:%M'
two_factor_auth: 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. description_html: Si vous activez <strong>l'identification à deux facteurs</strong>, vous devrez être en possession de votre téléphone afin de générer un code de connexion.
disable: Désactiver disable: Désactiver
enable: Activer enable: Activer
instructions_html: "<strong>Scannez ce QR code grâce à Google Authenticator, Authy ou une application similaire sur votre téléphone</strong>. Désormais, cette application générera des jetons que vous devrez saisir à chaque connexion." instructions_html: "<strong>Scannez ce QR code grâce à Google Authenticator, Authy ou une application similaire sur votre téléphone</strong>. Désormais, cette application générera des jetons que vous devrez saisir à chaque connexion."

View File

@ -3,7 +3,7 @@ de:
simple_form: simple_form:
hints: hints:
defaults: defaults:
locked: Erlaubt dir, Folger zu überprüfen, bevor sie dir folgen können locked: Erlaubt dir, Nutzer zu überprüfen, bevor sie dir folgen können
labels: labels:
defaults: defaults:
avatar: Avatar avatar: Avatar
@ -11,16 +11,16 @@ de:
confirm_password: Passwort bestätigen confirm_password: Passwort bestätigen
current_password: Derzeitiges Passwort current_password: Derzeitiges Passwort
display_name: Anzeigename display_name: Anzeigename
email: E-mail-Addresse email: E-Mail-Addresse
header: Kopfbild header: Kopfbild
locale: Sprache locale: Sprache
locked: Gesperrter Profil locked: Gesperrtes Profil
new_password: Neues Passwort new_password: Neues Passwort
note: Über mich note: Über mich
password: Passwort password: Passwort
username: Nutzername username: Nutzername
interactions: interactions:
must_be_follower: Benachrichtigungen von nicht-Folgern blockieren must_be_follower: Benachrichtigungen von Nicht-Folgern blockieren
must_be_following: Benachrichtigungen von Nutzern blockieren, denen ich nicht folge must_be_following: Benachrichtigungen von Nutzern blockieren, denen ich nicht folge
notification_emails: notification_emails:
favourite: E-mail senden, wenn jemand meinen Beitrag favorisiert favourite: E-mail senden, wenn jemand meinen Beitrag favorisiert

View File

@ -33,7 +33,7 @@ fr:
must_be_follower: Masquer les notifications des personnes qui ne vous suivent pas 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 must_be_following: Masquer les notifications des personnes que vous ne suivez pas
notification_emails: notification_emails:
digest: Envoyer des emails récapitulatifs digest: Envoyer des courriels récapitulatifs
favourite: Envoyer un courriel lorsque quelquun ajoute mes statuts à ses favoris favourite: Envoyer un courriel lorsque quelquun ajoute mes statuts à ses favoris
follow: Envoyer un courriel lorsque quelquun me suit follow: Envoyer un courriel lorsque quelquun me suit
follow_request: Envoyer un courriel lorsque quelqu'un demande à me suivre follow_request: Envoyer un courriel lorsque quelqu'un demande à me suivre

View File

@ -10,7 +10,7 @@ These people make the development of Mastodon possible through [Patreon](https:/
- [Kurtis Rainbolt-Greene](https://mastodon.social/users/krainboltgreene) - [Kurtis Rainbolt-Greene](https://mastodon.social/users/krainboltgreene)
- [Kit Redgrave](https://socially.constructed.space/users/KitRedgrave) - [Kit Redgrave](https://socially.constructed.space/users/KitRedgrave)
- [Zeipher](https://mastodon.social/users/Zeipher) - [Zeipher](https://mastodon.social/users/Zeipher)
- [Effy Elden](https://toot.zone/users/effy) - [Effy Elden](https://mastodon.social/users/effy)
- [Zoë Quinn](https://mastodon.social/users/zoequinn) - [Zoë Quinn](https://mastodon.social/users/zoequinn)
**Thank you to the following people** **Thank you to the following people**

View File

@ -39,6 +39,40 @@ You will want Amazon S3 for file storage. The only exception is for development
purposes, where you may not care if files are not saved. Follow a guide online purposes, where you may not care if files are not saved. Follow a guide online
for creating a free Amazon S3 bucket and Access Key, then enter the details. for creating a free Amazon S3 bucket and Access Key, then enter the details.
If you deploy from the web, the format for all the S3 bits use Paperclip conventions:
S3 Bucket is just the name of the bucket, e.g. `bucketname` not the full ARN.
S3 Region is the AWS code for the region e.g. `ap-northeast-1` not the name of the city displayed on the AWS Dashboard.
To protect the privacy of the users of the your instance, you should have permissons on the your S3 bucket set to no-read and no-write for the public and non-application-specific AWS users, with only one authorized IAM user or group set up to be able to upload or display content. This is an example of an IAM policy used for the S3 bucket used Mastadon instance hentai.loan:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets"
],
"Resource": [
"arn:aws:s3:::*"
]
},
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::hentailoan”,
"arn:aws:s3:::hentailoan/*"
]
}
]
}
## Deployment ## Deployment
You can deploy from the Heroku web interface or from the command line. Run: You can deploy from the Heroku web interface or from the command line. Run:

View File

@ -90,7 +90,9 @@ It is recommended to create a special user for mastodon on the server (you could
sudo apt-get install imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev nodejs file git curl sudo apt-get install imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev nodejs file git curl
curl -sL https://deb.nodesource.com/setup_4.x | sudo bash - curl -sL https://deb.nodesource.com/setup_4.x | sudo bash -
apt-get install nodejs
sudo apt-get install nodejs
sudo npm install -g yarn sudo npm install -g yarn
## Redis ## Redis

View File

@ -7,7 +7,7 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz)
| -------------|-------------|---|---| | -------------|-------------|---|---|
| [mastodon.social](https://mastodon.social) |Flagship, quick updates|No|No| | [mastodon.social](https://mastodon.social) |Flagship, quick updates|No|No|
| [securitymastod.one](https://securitymastod.one/) |Information security enthusiasts and pros|Yes|Yes| | [securitymastod.one](https://securitymastod.one/) |Information security enthusiasts and pros|Yes|Yes|
| [mastodon.nuzgo.net](https://mastodon.nuzgo.net/) |Mastodon instance hosted in Paris |Yes|No| | [mastodon.nuzgo.net](https://mastodon.nuzgo.net/) |Mastodon instance hosted in Paris |Yes|Yes|
| [mastodon.cx](https://mastodon.cx/) |Alternative Mastodon instance hosted in France|Yes|Yes| | [mastodon.cx](https://mastodon.cx/) |Alternative Mastodon instance hosted in France|Yes|Yes|
| [mastodon.network](https://mastodon.network) |N/A|Yes|Yes| | [mastodon.network](https://mastodon.network) |N/A|Yes|Yes|
| [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|Yes|No| | [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|Yes|No|
@ -69,7 +69,7 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz)
| [meow.social](https://meow.social)|A furry fandom focused instance|Yes|No| | [meow.social](https://meow.social)|A furry fandom focused instance|Yes|No|
| [neumastodon.com](https://neumastodon.com/)|Northeastern University Mastodon |Yes|No| | [neumastodon.com](https://neumastodon.com/)|Northeastern University Mastodon |Yes|No|
| [dancingbanana.party](https://dancingbanana.party)|La banane qui danse.|Yes|No| | [dancingbanana.party](https://dancingbanana.party)|La banane qui danse.|Yes|No|
| [mastodon.brussels.fr](https://mastodon.brussels/)|Le mastodon pour les belges, si vous aimez la bonne ambiance venez nous rejoindre !|Yes|Yes| | [mastodon.brussels](https://mastodon.brussels/)|Le mastodon pour les belges, si vous aimez la bonne ambiance venez nous rejoindre !|Yes|Yes|
| [mastodon.llamasweet.tech](https://mastodon.llamasweet.tech/)|Mastodon about Android developement|Yes|No| | [mastodon.llamasweet.tech](https://mastodon.llamasweet.tech/)|Mastodon about Android developement|Yes|No|
| [manx.social](https://manx.social/)|Instance for the Isle of Man|Yes|Yes| | [manx.social](https://manx.social/)|Instance for the Isle of Man|Yes|Yes|
| [mastodon.host](https://mastodon.host/)|Lightly moderated, federates everywhere and has a follow bot ( Huge federated timeline )|Yes|No| | [mastodon.host](https://mastodon.host/)|Lightly moderated, federates everywhere and has a follow bot ( Huge federated timeline )|Yes|No|
@ -77,6 +77,7 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz)
| [oulipo.social](https://oulipo.social/)|An Oulipo Mastodon in which that fifth symbol in Latin script is taboo|Yes|No| | [oulipo.social](https://oulipo.social/)|An Oulipo Mastodon in which that fifth symbol in Latin script is taboo|Yes|No|
| [indigo.zone](https://indigo.zone)|Open Registrations, General Purpose|Yes|No| | [indigo.zone](https://indigo.zone)|Open Registrations, General Purpose|Yes|No|
| [mastodones.club](https://mastodones.club)|Mastodon en español|Yes|Yes| | [mastodones.club](https://mastodones.club)|Mastodon en español|Yes|Yes|
| [mst3k.interlinked.me](https://mst3k.interlinked.me)|Open registrations, general purpose|Yes|Yes|
Let me know if you start running one so I can add it to the list! (Alternatively, add it yourself as a pull request). We are no longer maintaining this list as instances are popping up too quickly for using GitHub to be a tenable system for tracking them. Please standby while we work on another solution

View File

@ -43,6 +43,7 @@ ___
- [For Python](https://github.com/halcy/Mastodon.py) - [For Python](https://github.com/halcy/Mastodon.py)
- [For JavaScript](https://github.com/Zatnosk/libodonjs) - [For JavaScript](https://github.com/Zatnosk/libodonjs)
- [For JavaScript (Node.js)](https://github.com/jessicahayley/node-mastodon) - [For JavaScript (Node.js)](https://github.com/jessicahayley/node-mastodon)
- [For Elixir](https://github.com/milmazz/hunter)
___ ___

View File

@ -0,0 +1,15 @@
require "rails_helper"
describe "site_title" do
it "Uses the Setting.site_title value when it exists" do
Setting.site_title = "New site title"
expect(helper.site_title).to eq "New site title"
end
it "returns empty string when Setting.site_title is nil" do
Setting.site_title = nil
expect(helper.site_title).to eq ""
end
end

View File

@ -1,7 +1,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe UpdateRemoteProfileService do RSpec.describe UpdateRemoteProfileService do
let(:xml) { Nokogiri::XML(File.read(File.join(Rails.root, 'spec', 'fixtures', 'push', 'feed.atom'))).at_xpath('//xmlns:feed') } let(:xml) { File.read(File.join(Rails.root, 'spec', 'fixtures', 'push', 'feed.atom')) }
subject { UpdateRemoteProfileService.new } subject { UpdateRemoteProfileService.new }