Merge branch 'master' into skylight

This commit is contained in:
Eugen Rochko 2017-04-08 13:24:25 +02:00
commit 7c81c0f7cb
63 changed files with 1037 additions and 104 deletions

View file

@ -29,6 +29,7 @@ OTP_SECRET=
# DEFAULT_LOCALE=de # DEFAULT_LOCALE=de
# E-mail configuration # E-mail configuration
# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
SMTP_SERVER=smtp.mailgun.org SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587 SMTP_PORT=587
SMTP_LOGIN= SMTP_LOGIN=

View file

@ -29,7 +29,8 @@ RUN BUILD_DEPS=" \
&& npm install -g npm@3 && npm install -g yarn \ && npm install -g npm@3 && npm install -g yarn \
&& bundle install --deployment --without test development \ && bundle install --deployment --without test development \
&& yarn \ && yarn \
&& npm cache clean \ && yarn cache clean \
&& npm -g cache clean \
&& apk del $BUILD_DEPS \ && apk del $BUILD_DEPS \
&& rm -rf /tmp/* /var/cache/apk/* && rm -rf /tmp/* /var/cache/apk/*

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

@ -47,6 +47,7 @@ import pt from 'react-intl/locale-data/pt';
import hu from 'react-intl/locale-data/hu'; import hu from 'react-intl/locale-data/hu';
import uk from 'react-intl/locale-data/uk'; import uk from 'react-intl/locale-data/uk';
import fi from 'react-intl/locale-data/fi'; import fi from 'react-intl/locale-data/fi';
import eo from 'react-intl/locale-data/eo';
import getMessagesForLocale from '../locales'; import getMessagesForLocale from '../locales';
import { hydrateStore } from '../actions/store'; import { hydrateStore } from '../actions/store';
import createStream from '../stream'; import createStream from '../stream';
@ -59,7 +60,7 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
basename: '/web' basename: '/web'
}); });
addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi]); addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo]);
const Mastodon = React.createClass({ const Mastodon = React.createClass({

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

@ -0,0 +1,68 @@
const eo = {
"column_back_button.label": "Reveni",
"lightbox.close": "Fermi",
"loading_indicator.label": "Ŝarĝanta...",
"status.mention": "Mencii @{name}",
"status.delete": "Forigi",
"status.reply": "Respondi",
"status.reblog": "Diskonigi",
"status.favourite": "Favori",
"status.reblogged_by": "{name} diskonigita",
"status.sensitive_warning": "Tikla enhavo",
"status.sensitive_toggle": "Alklaki por vidi",
"video_player.toggle_sound": "Aktivigi sonojn",
"account.mention": "Mencii @{name}",
"account.edit_profile": "Redakti la profilon",
"account.unblock": "Malbloki @{name}",
"account.unfollow": "Malsekvi",
"account.block": "Bloki @{name}",
"account.follow": "Sekvi",
"account.posts": "Mesaĝoj",
"account.follows": "Sekvatoj",
"account.followers": "Sekvantoj",
"account.follows_you": "Sekvas vin",
"account.requested": "Atendas aprobon",
"getting_started.heading": "Por komenci",
"getting_started.about_addressing": "Vi povas sekvi homojn se vi konas la uzantnomon kaj domajnon tajpinte retpoŝtecan adreson en la serĉilon.",
"getting_started.about_shortcuts": "Se la celita uzanto troviĝas en la sama domajno de vi, uzi nur la uzantnomon sufiĉos. La sama regulo validas por mencii aliajn uzantojn en mesaĝo.",
"getting_started.open_source_notice": "Mastodon estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en github je {github}. {apps}.",
"column.home": "Hejmo",
"column.community": "Loka tempolinio",
"column.public": "Fratara tempolinio",
"column.notifications": "Sciigoj",
"tabs_bar.compose": "Ekskribi",
"tabs_bar.home": "Hejmo",
"tabs_bar.mentions": "Sciigoj",
"tabs_bar.public": "Fratara tempolinio",
"tabs_bar.notifications": "Sciigoj",
"compose_form.placeholder": "Pri kio vi pensas?",
"compose_form.publish": "Hup",
"compose_form.sensitive": "Marki ke la enhavo estas tikla",
"compose_form.spoiler": "Kaŝi la tekston malantaŭ averto",
"compose_form.private": "Marki ke la enhavo estas privata",
"compose_form.privacy_disclaimer": "Via privata mesaĝo estos sendita nur al menciitaj uzantoj en {domains}. Ĉu vi fidas {domainsCount, plural, one {tiun servilon} other {tiujn servilojn}}? Mesaĝa privateco funkcias nur en aperaĵoj de Mastodon. Se {domains} {domainsCount, plural, one {ne estas aperaĵo de Mastodon} other {ne estas aperaĵoj de Mastodon}}, estos neniu indiko ke via mesaĝo estas privata, kaj ĝi povus esti diskonigita aŭ videbligita al necelitaj ricevantoj.",
"compose_form.unlisted": "Ne afiŝi en publikaj tempolinioj",
"navigation_bar.edit_profile": "Redakti la profilon",
"navigation_bar.preferences": "Preferoj",
"navigation_bar.community_timeline": "Loka tempolinio",
"navigation_bar.public_timeline": "Fratara tempolinio",
"navigation_bar.logout": "Elsaluti",
"reply_indicator.cancel": "Rezigni",
"search.placeholder": "Serĉi",
"search.account": "Konto",
"search.hashtag": "Kradvorto",
"upload_button.label": "Aldoni enhavaĵon",
"upload_form.undo": "Malfari",
"notification.follow": "{name} sekvis vin",
"notification.favourite": "{name} favoris vian mesaĝon",
"notification.reblog": "{name} diskonigis vian mesaĝon",
"notification.mention": "{name} menciis vin",
"notifications.column_settings.alert": "Retumilaj atentigoj",
"notifications.column_settings.show": "Montri en kolono",
"notifications.column_settings.follow": "Novaj sekvantoj:",
"notifications.column_settings.favourite": "Favoroj:",
"notifications.column_settings.mention": "Mencioj:",
"notifications.column_settings.reblog": "Diskonigoj:",
};
export default eo;

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

@ -6,6 +6,7 @@ import fr from './fr';
import pt from './pt'; import pt from './pt';
import uk from './uk'; import uk from './uk';
import fi from './fi'; import fi from './fi';
import eo from './eo';
const locales = { const locales = {
en, en,
@ -15,7 +16,8 @@ const locales = {
fr, fr,
pt, pt,
uk, uk,
fi fi,
eo
}; };
export default function getMessagesForLocale (locale) { export default function getMessagesForLocale (locale) {

View file

@ -1,5 +1,9 @@
@import 'variables'; @import 'variables';
.app-body{
-ms-overflow-style: -ms-autohiding-scrollbar;
}
.button { .button {
background-color: darken($color4, 3%); background-color: darken($color4, 3%);
font-family: inherit; font-family: inherit;

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

@ -11,6 +11,7 @@ module SettingsHelper
uk: 'Українська', uk: 'Українська',
'zh-CN': '简体中文', 'zh-CN': '简体中文',
fi: 'Suomi', fi: 'Suomi',
eo: 'Esperanto',
}.freeze }.freeze
def human_locale(locale) def human_locale(locale)

View file

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

View file

@ -34,10 +34,6 @@ module StreamEntriesHelper
user_signed_in? && @favourited.key?(status.id) ? 'favourited' : '' user_signed_in? && @favourited.key?(status.id) ? 'favourited' : ''
end end
def proper_status(status)
status.reblog? ? status.reblog : status
end
def rtl?(text) def rtl?(text)
return false if text.empty? return false if text.empty?

View file

@ -328,7 +328,7 @@ class AtomSerializer
def serialize_status_attributes(entry, status) def serialize_status_attributes(entry, status)
append_element(entry, 'summary', status.spoiler_text) unless status.spoiler_text.blank? append_element(entry, 'summary', status.spoiler_text) unless status.spoiler_text.blank?
append_element(entry, 'content', Formatter.instance.format(status.reblog? ? status.reblog : status).to_str, type: 'html') append_element(entry, 'content', Formatter.instance.format(status.proper).to_str, type: 'html')
status.mentions.each do |mentioned| status.mentions.each do |mentioned|
append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:person], href: TagManager.instance.uri_for(mentioned.account)) append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:person], href: TagManager.instance.uri_for(mentioned.account))

View file

@ -125,11 +125,11 @@ class Account < ApplicationRecord
end end
def favourited?(status) def favourited?(status)
(status.reblog? ? status.reblog : status).favourites.where(account: self).count.positive? status.proper.favourites.where(account: self).count.positive?
end end
def reblogged?(status) def reblogged?(status)
(status.reblog? ? status.reblog : status).reblogs.where(account: self).count.positive? status.proper.reblogs.where(account: self).count.positive?
end end
def keypair def keypair

View file

@ -62,8 +62,12 @@ class Status < ApplicationRecord
reply? ? :comment : :note reply? ? :comment : :note
end end
def proper
reblog? ? reblog : self
end
def content def content
reblog? ? reblog.text : text proper.text
end end
def target def target

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

@ -37,11 +37,11 @@ class PostStatusService < BaseService
def validate_media!(media_ids) def validate_media!(media_ids)
return if media_ids.nil? || !media_ids.is_a?(Enumerable) return if media_ids.nil? || !media_ids.is_a?(Enumerable)
raise Mastodon::ValidationError, 'Cannot attach more than 4 files' if media_ids.size > 4 raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if media_ids.size > 4
media = MediaAttachment.where(status_id: nil).where(id: media_ids.take(4).map(&:to_i)) media = MediaAttachment.where(status_id: nil).where(id: media_ids.take(4).map(&:to_i))
raise Mastodon::ValidationError, 'Cannot attach a video to a toot that already contains images' if media.size > 1 && media.find(&:video?) raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if media.size > 1 && media.find(&:video?)
media media
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

@ -16,7 +16,7 @@
%strong= display_name(status.account) %strong= display_name(status.account)
= t('stream_entries.reblogged') = t('stream_entries.reblogged')
= render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: proper_status(status) } = render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: status.proper }
- if include_threads - if include_threads
= render partial: 'stream_entries/status', collection: @descendants, as: :status, locals: { is_successor: true } = render partial: 'stream_entries/status', collection: @descendants, as: :status, locals: { is_successor: true }

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

@ -24,7 +24,7 @@ module Mastodon
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN', :fi] config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN', :fi, :eo]
config.i18n.default_locale = :en config.i18n.default_locale = :en
# config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')

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

@ -0,0 +1,61 @@
---
eo:
devise:
confirmations:
confirmed: Via konto estas konfirmita.
send_instructions: Vi ricevos instrukciojn por konfirmi vian konton post kelkaj minutoj.
send_paranoid_instructions: Se via retpoŝt-adreso ekzistas en nia datumbazo, vi baldaŭ ricevos retpoŝt-mesaĝon, kiu enhavas la instrukciojn por konfirmi vian konton.
failure:
already_authenticated: Vi jam estas ensalutita.
inactive: Via konto ankoraŭ ne estas konfirmita.
invalid: Malĝusta retpoŝt-adreso aŭ pasvorto.
last_attempt: Vi ankoraŭ povas provi unufoje antaŭ ol via konto estos ŝlosita.
locked: Via konto estas ŝlosita.
not_found_in_database: Malĝusta retpoŝt-adreso aŭ pasvorto.
timeout: Via sesio eksiĝis. Bonvolu reensaluti por daŭrigi.
unauthenticated: Vi devas ensaluti aŭ membriĝi por daŭrigi.
unconfirmed: Vi devas konfirmi vian konton por daŭrigi.
mailer:
confirmation_instructions:
subject: Instrukcioj por konfirmi
password_change:
subject: Via pasvorto estis ŝanĝita senprobleme.
reset_password_instructions:
subject: Instrukcioj por ŝanĝi la pasvorton
unlock_instructions:
subject: Instrukcioj por malŝlosi la konton
omniauth_callbacks:
failure: 'Ni ne povis aŭtentigi vin per %{kind}: ''%{reason}''.'
success: Aŭtentigita senprobleme per %{kind}.
passwords:
no_token: Vi ne povas iri al tiu paĝo per alia vojo ol retpoŝt-mesaĝo por ŝanĝi pasvorton. Se vi venas de tia retpoŝt-mesaĝo, kontrolu ke vi uzis la tutan URL.
send_instructions: Vi ricevos retpoŝt-mesaĝon kun instrukcioj por ŝanĝi vian pasvorton post kelkaj minutoj.
send_paranoid_instructions: Se via retpoŝt-adreso ekzistas en nia datumbazo, vi ricevos ligilon por ŝanĝi vian pasvorton per retpoŝt-mesaĝo.
updated: Via pasvorto estis redaktita senprobleme, vi nun estas ensalutita.
updated_not_active: Via pasvorto estis redaktita senprobleme.
registrations:
destroyed: Ĝis! Via konto estis forigita senprobleme. Ni esperas revidi vin baldaŭ.
signed_up: Bonvenon! Vi membriĝis senprobleme.
signed_up_but_inactive: Vi bone membriĝis, sed vi ankoraŭ ne povas ensaluti ĉar via konto ne estis konfirmita.
signed_up_but_locked: Vi bone membriĝis, sed vi ne povas ensaluti ĉar via konto estas ŝlosita.
signed_up_but_unconfirmed: Retpoŝt-mesaĝo kun via ligilo por konfirmi vian konton estis sendita al via retpoŝt-adreso. Bonvolu uzi tiun ligilon por konfirmi vian konton.
update_needs_confirmation: Vi bone aktualigis vian konton, sed ni bezonas kontroli vian novan retpoŝt-adreson. Bonvolu kontroli viajn retpoŝt-mesaĝojn kaj uzi la ligilon por konfirmi vian novan retpoŝt-adreson.
updated: Via konto estis aktualigita senprobleme.
sessions:
already_signed_out: Elsalutita.
signed_in: Ensalutita.
signed_out: Elsalutita.
unlocks:
send_instructions: Vi ricevos retpoŝt-mesaĝon kun instrukcioj por malŝlosi vian konton post kelkaj minutoj.
send_paranoid_instructions: Se via retpoŝt-adreso ekzistas en nia datumbazo, vi ricevos ligilon por malŝlosi vian konton per retpoŝt-mesaĝo.
unlocked: Via konto estis malŝlosita senprobleme, vi nun estas ensalutita.
errors:
messages:
already_confirmed: jam estis konfirmita, bonvolu provi ensaluti
confirmation_period_expired: devas esti konfirmita en %{period}, bonvolu repeti
expired: eksiĝis, bonvolu repeti
not_found: ne estis trovita
not_locked: ne estis ŝlosita
not_saved:
one: '1 eraro malpermesis al tiu %{resource} esti konservita:'
other: '%{count} eraroj malpermesis al tiu %{resource} esti konservita:'

View file

@ -0,0 +1,113 @@
---
eo:
activerecord:
attributes:
doorkeeper/application:
name: Nomo
redirect_uri: URI de plusendo
errors:
models:
doorkeeper/application:
attributes:
redirect_uri:
fragment_present: ne povas enhavi eron.
invalid_uri: devas esti valida URI.
relative_uri: devas esti absoluta URI.
secured_uri: devas esti HTTPS/SSL-a URI.
doorkeeper:
applications:
buttons:
authorize: Rajtigi
cancel: Rezigni
destroy: Detrui
edit: Redakti
submit: Sendi
confirmations:
destroy: Ĉu vi certas?
edit:
title: Redakti aplikaĵon
form:
error: Ups! Kontrolu vian formularon ĉu estas eraroj
help:
native_redirect_uri: Uzu %{native_redirect_uri} por lokaj provoj
redirect_uri: Uzu unu linion por ĉiu URI
scopes: Apartigu ampleksojn per spacetoj. Lasu malplena por uzi la senŝanĝajn ampleksojn.
index:
callback_url: URL vokita per referenco
name: Nomo
new: Nova Aplikaĵo
title: Viaj aplikaĵoj
new:
title: Nova aplikaĵo
show:
actions: Agoj
application_id: Identigo de la aplikaĵo
callback_urls: URL-j vokitaj per referenco
scopes: Ampleksoj
secret: Sekreto
title: 'Aplikaĵo: %{name}'
authorizations:
buttons:
authorize: Rajtigi
deny: Rifuzi
error:
title: Eraro okazis
new:
able_to: Povos
prompt: La aplikaĵo %{client_name} petas aliron al via konto
title: Rajtigo bezonata
show:
title: Rajtiga kodo
authorized_applications:
buttons:
revoke: Malrajtigi
confirmations:
revoke: Ĉu vi certas?
index:
application: Aplikaĵo
created_at: Rajtigita
date_format: "%Y-%m-%d %H:%M:%S"
scopes: Ampleksoj
title: Viaj rajtigitaj aplikaĵoj
errors:
messages:
access_denied: La posedanto de la rimedo aŭ la rajtiga servilo rifuzis vian peton.
credential_flow_not_configured: La sendado de la identigiloj de la posedanto de la rimedo malsukcesis ĉar Doorkeeper.configure.resource_owner_from_credentials ne estis agordita.
invalid_client: La aŭtentigo de la kliento malsukcesis ĉar la kliento estas nekonata, aŭ mankis peto aŭtentigi, aŭ la aŭtentig-metodo ne estas subtenata.
invalid_grant: La rajtiga konsento ne estas valida, ne plu estas valida, estis forigita, ne kongruas kun la plusenda URI uzita en la aŭtentiga peto, aŭ estis sendita al alia kliento.
invalid_redirect_uri: La plusenda URI uzita en estas valida.
invalid_request: Mankis al la peto nepra parametro, enhavas nesubtenatan parametran valoron, aŭ la peto simple estas misformita.
invalid_resource_owner: La donitaj identigiloj pri la posedanto de la rimedo ne estas validaj, aŭ tiu ne povas esti trovita.
invalid_scope: La petita amplekso ne estas valida, estas nekonata, aŭ estas misformita.
invalid_token:
expired: La atingoĵetono eskiĝis.
revoked: La atingoĵetono estis rifuzita.
unknown: La atingoĵetono ne estas valida.
resource_owner_authenticator_not_configured: La posedanto de la rimedo ne povis esti trovita ĉar Doorkeeper.configure.resource_owner_authenticator ne estas agordita.
server_error: La rajtiga servilo rimarkis neatenditan kondiĉon, kiu malpermesis al ĝi plenumi la peton.
temporarily_unavailable: La rajtiga servilo ne povas nun plenumi la peton pro dumtempa superŝarĝo aŭ prizorgado de la servilo.
unauthorized_client: La kliento ne rajtas fari tian peton uzante tiun metodon.
unsupported_grant_type: La tipo de la rajtiga konsento ne estas subtenata de la rajtiga servilo.
unsupported_response_type: La rajtiga servilo ne subtenas tian respondon.
flash:
applications:
create:
notice: Aplikaĵo kreita.
destroy:
notice: Aplikaĵo forigita.
update:
notice: Aplikaĵo aktualigita.
authorized_applications:
destroy:
notice: Aplikaĵo malrajtigita.
layouts:
admin:
nav:
applications: Aplikaĵoj
oauth2_provider: OAuth2-provizanto
application:
title: OAuth-a rajtigo bezonata
scopes:
follow: sekvi, bloki, malbloki kaj malsekvi kontojn
read: legi la datumojn de via konto
write: mesaĝi kiel vi

View file

@ -163,3 +163,7 @@ en:
invalid_otp_token: Invalid two-factor code invalid_otp_token: Invalid two-factor code
will_paginate: will_paginate:
page_gap: "&hellip;" page_gap: "&hellip;"
media_attachments:
validations:
too_many: Cannot attach more than 4 files
images_and_video: Cannot attach a video to a status that already contains images

164
config/locales/eo.yml Normal file
View file

@ -0,0 +1,164 @@
---
eo:
about:
about_mastodon: Mastodon estas <em>senpaga, malfermitkoda</em> socia reto. Ĝi estas <em>sencentra</em> alia eblo al komercaj servoj. Ĝi evitigas, ke unusola firmao regu vian tutan komunikadon. Elektu servilon, kiun vi fidas. Kiu ajn estas via elekto, vi povas interagi kun ĉiuj aliaj uzantoj. Iu ajn povas krei sian propran aperaĵon de Mastodon en sia servilo, kaj partopreni en la <em>socia reto</em> tute glate.
about_this: Pri tiu aperaĵo
apps: Aplikaĵoj
business_email: 'Profesia retpoŝt-adreso:'
contact: Kontakti
description_headline: Kio estas %{domain}?
domain_count_after: aliaj aperaĵoj
domain_count_before: Konektita al
features:
api: Malfermita API por aplikaĵoj kaj servoj
blocks: Kompletaj iloj por bloki kaj kaŝi
characters: Po 500 signoj por ĉiu mesaĝo
chronology: Tempolinioj laŭtempaj
ethics: 'Etike kreita: neniu reklamo, neniu ŝpurado'
gifv: Eblo diskonigi etajn videojn kaj GIFV
privacy: Videbleco agordita laŭ la mesaĝo
public: Publikaj tempolinioj
features_headline: Kiel Mastodon estas malsimila
get_started: Komenci
links: Ligiloj
other_instances: Aliaj aperaĵoj
source_code: Fontkodo
status_count_after: mesaĝoj
status_count_before: Kiu publikigis
terms: Terms
user_count_after: uzantoj
user_count_before: Hejmo de
accounts:
follow: Sekvi
followers: Sekvantoj
following: Sekvatoj
nothing_here: Estas nenio ĉi tie!
people_followed_by: Sekvatoj de %{name}
people_who_follow: Sekvantoj de %{name}
posts: Mesaĝoj
remote_follow: Fore sekvi
unfollow: Malsekvi
application_mailer:
settings: 'Ŝanĝi la retpoŝt-mesaĝajn preferojn: %{link}'
signature: Sciigoj de Mastodon el %{instance}
view: 'Vidi:'
applications:
invalid_url: La URL donita ne estas valida
auth:
change_password: Ŝanĝi pasvorton
didnt_get_confirmation: Ĉu vi ne ricevis la instrukciojn por konfirmi?
forgot_password: Pasvorto forgesita?
login: Ensaluti
logout: Elsaluti
register: Membriĝi
resend_confirmation: Resendi la instrukciojn por konfirmi
reset_password: Ŝanĝi la pasvorton
set_new_password: Elekti novan pasvorton
authorize_follow:
error: Bedaŭrinde, okazis eraro provante konsulti la foran konton
follow: Sekvi
prompt_html: 'Vi (<strong>%{self}</strong>) petis sekvi:'
title: Sekvi %{acct}
datetime:
distance_in_words:
about_x_hours: "%{count}h"
about_x_months: "%{count}mo"
about_x_years: "%{count}j"
almost_x_years: "%{count}j"
half_a_minute: Ĵus
less_than_x_minutes: "%{count}m"
less_than_x_seconds: Ĵus
over_x_years: "%{count}j"
x_days: "%{count}t"
x_minutes: "%{count}m"
x_months: "%{count}mo"
x_seconds: "%{count}s"
exports:
blocks: Vi blokas
csv: CSV
follows: Vi sekvas
storage: Mediaĵa konservado
generic:
changes_saved_msg: Ŝanĝoj senprobleme konservitaj!
powered_by: povigita de %{link}
save_changes: Konservi la ŝanĝojn
validation_errors:
one: Io ne okazis senprobleme! Bonvolu konsulti la suban erar-raporton.
other: Io ne okazis senprobleme! Bonvolu konsulti la subajn %{count} erar-raportojn.
imports:
preface: Vi povas alporti kelkajn datumojn, kiel listojn de ĉiuj homoj kiujn vi sekvas aŭ blokas, al via konto de ĉi tiu aperaĵo, per dosiero elportita de alia aperaĵo.
success: Viaj datumoj estis senprobleme alportitaj kaj estos traktitaj kiel planite.
types:
blocking: Listo de blokitoj
following: Listo de sekvatoj
upload: Alporti
landing_strip_html: <strong>%{name}</strong> estas uzanto en <strong>%{domain}</strong>. Vi povas sekvi tiun aŭ interagi kun tiu, se vi havas konton ie ajn en la Fediverse. Se vi ne havas, vi povas <a href="%{sign_up_path}">membriĝi ĉi tie.</a>.
notification_mailer:
digest:
body: 'Jen eta resumo de tio, kio okazis en %{instance}, ekde kiam vi laste vizitis en %{since}:'
mention: "%{name} menciis vin en:"
new_followers_summary:
one: Vi ekhavis novan sekvanton! Jej!
other: Vi ekhavis %{count} novajn sekvantojn! Mirinde!
subject:
one: "1 nova sciigo ekde via lasta vizito \U0001F418"
other: "%{count} novaj sciigoj ekde via lasta vizito \U0001F418"
favourite:
body: '%{name} favoris vian mesaĝon:'
subject: "%{name} favoris vian mesaĝon"
follow:
body: "%{name} eksekvis vin:"
subject: "%{name} eksekvis vin"
follow_request:
body: "%{name} petis sekvi vin:"
subject: '%{name} petis sekvi vin'
mention:
body: '%{name} menciis vin en:'
subject: '%{name} menciis vin'
reblog:
body: '%{name} diskonigis vian mesaĝon:'
subject: "%{name} diskonigis vian mesaĝon"
pagination:
next: Sekva
prev: Malsekva
remote_follow:
acct: Enmetu vian uzantnomo@aperaĵo de kie vi volas sekvi tiun uzanton
missing_resource: La URL de plusendado ne povis esti trovita
proceed: Daŭrigi por plusendi
prompt: 'Vi eksekvos:'
settings:
authorized_apps: Rajtigitaj aplikaĵoj
back: Reveni al Mastodon
edit_profile: Redakti la profilon
export: Elporti datumojn
import: Alporti
preferences: Preferoj
settings: Agordoj
two_factor_auth: Dufaktora aŭtentigo
statuses:
open_in_web: Malfermi retumile
over_character_limit: limo de %{max} signoj trapasita
show_more: Montri pli
visibilities:
private: Montri nur al sekvantoj
public: Publika
unlisted: Publika, sed ne aperos en publikaj tempolinioj
stream_entries:
click_to_show: Alklaki por montri
reblogged: diskonigita
sensitive_content: Tikla enhavo
time:
formats:
default: "%b %d, %Y, %H:%M"
two_factor_auth:
description_html: Se vi ebligas <strong>dufaktoran aŭtentigon</strong>, vi bezonos vian poŝtelefonon por ensaluti, ĉar ĝi kreos nombrojn, kiujn vi devos entajpi.
disable: Malebligi
enable: Ebligi
instructions_html: "<strong>Skanu tiun QR-kodon per Google Authenticator aŭ per simila aplikaĵo de via poŝtelefono</strong>. De tiam, la aplikaĵo kreos nombrojn, kiujn vi devos entajpi."
plaintext_secret_html: 'Rekte legebla sekreta kodo: <samp>%{secret}</samp>'
warning: Se vi ne povas agordi aŭtentigan aplikaĵon nun, elektu "malebligi" aŭ vi ne plu povos ensaluti.
users:
invalid_email: La retpoŝt-adreso ne estas valida
invalid_otp_token: La dufaktora aŭtentigila kodo ne estas valida
will_paginate:
page_gap: "&hellip;"

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

@ -0,0 +1,46 @@
---
eo:
simple_form:
hints:
defaults:
avatar: En la formato PNG, GIF aŭ JPG. Ĝis 2Mo. Estos malgrandigita al 120x120px
display_name: 30 signoj pleje
header: En la formato PNG, GIF aŭ JPG. Ĝis 2Mo. Estos malgrandigita al 700x335px
locked: Vi devos aprobi ĉiun peton de sekvado, kaj viaj mesaĝoj estos senŝanĝe nur por viaj sekvantoj.
note: 160 signoj pleje
imports:
data: Dosiero CSV el alia aperaĵo de Mastodon
labels:
defaults:
avatar: Profilbildo
confirm_new_password: Konfirmi novan pasvorton
confirm_password: Konfirmi la pasvorton
current_password: Nuna pasvorto
data: Datumoj
display_name: Publika nomo
email: Retpoŝt-adreso
header: Kapbildo
locale: Lingvo
locked: Privatigi la konton
new_password: Nova pasvorto
note: Sinprezento
otp_attempt: Dufaktora identigilo
password: Pasvorto
setting_default_privacy: Videbleco de la mesaĝoj
type: Tipo de alportado
username: Uzantnomo
interactions:
must_be_follower: Kaŝi la sciigojn de homoj, kiuj ne sekvas vin
must_be_following: Kaŝi la sciigojn de homoj, kiujn vi ne sekas
notification_emails:
digest: Sendi resumajn retpoŝt-mesaĝojn
favourite: Sendi retpoŝt-mesaĝon, kiam iu favoras mesaĝon de vi
follow: Sendi retpoŝt-mesaĝon, kiam iu eksekvas vin
follow_request: Sendi retpoŝt-mesaĝon, kiam iu petas sekvi vin
mention: Sendi retpoŝt-mesaĝon, kiam iu mencias vin
reblog: Sendi retpoŝt-mesaĝon, kiam iu diskonigas mesaĝon de vi
'no': 'Ne'
required:
mark: "*"
text: bezonata
'yes': 'Jes'

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

@ -7,7 +7,7 @@ So, you have a working Mastodon instance... now what?
The following rake task: The following rake task:
rake mastodon:make_admin USERNAME=alice RAILS_ENV=production bundle exec rails mastodon:make_admin USERNAME=alice
Would turn the local user "alice" into an admin. Would turn the local user "alice" into an admin.

View file

@ -3,13 +3,84 @@ Heroku guide
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?button-url=https://github.com/tootsuite/mastodon&template=https://github.com/tootsuite/mastodon) [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?button-url=https://github.com/tootsuite/mastodon&template=https://github.com/tootsuite/mastodon)
Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. It should be noted this has limited testing and could have unpredictable results. Mastodon can be run on a free [Heroku](https://heroku.com) app. It should be
noted this has limited testing and could have unpredictable results.
1. Click the above button. ## Basic setup
2. Fill in the options requested.
* You can use a .herokuapp.com domain, which will be simple to set up, or you can use a custom domain. If you want a custom domain and HTTPS, you will need to upgrade to a paid plan (to use Heroku's SSL features), or set up [CloudFlare](https://cloudflare.com) who offer free "Flexible SSL" (note: CloudFlare have some undefined limits on WebSockets. So far, no one has reported hitting concurrent connection limits).
* 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 for creating a free Amazon S3 bucket and Access Key, then enter the details.
* If you want your Mastodon to be able to send emails, configure SMTP settings here (or later). Consider using [Mailgun](https://mailgun.com) or similar, who offer free plans that should suit your interests.
3. Deploy! The app should be set up, with a working web interface and database. You can change settings and manage versions from the Heroku dashboard.
You may need to use the `heroku` CLI application to run `USERNAME=yourUsername rails mastodon:make_admin` to make yourself an admin. Click the button above to start creating a Heroku app with the Mastodon repo as
the source. This tells Heroku to use the `app.json` file which does things like
prompt for config variables, set up the right buildpacks, run a postdeploy task,
and add the appropriate addons.
If you don't use the deploy button and app.json approach, you will need to do
some of that manually.
## Domain names and SSL
You can add your domain name to the Heroku app's setting, and then also use
Heroku's (free) auto renewal program for Lets Encrypt certificates, by
requesting a cert from the settings screen. You'll have to point your hostname
DNS at Heroku using the values heroku gives you on this screen, using whatever
method is appropriate for your DNS setup.
You should set the Heroku config vars of `LOCAL_DOMAIN` to your hostname, and
`LOCAL_HTTPS` to "true" as well.
## Email
Consider using [Mailgun](https://mailgun.com) or similar, who offer free plans
that should suit your interests. Look in `production.rb` to see which config
variables need to be set on Heroku for outgoing email to work.
## File storage
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
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
You can deploy from the Heroku web interface or from the command line. Run:
`heroku run rails db:migrate`
after you first deploy to set up the first database.
To make yourself an admin, you may need to use the `heroku` CLI application after creating an account online:
`heroku rake mastodon:make_admin USERNAME=yourUsername`

View file

@ -24,7 +24,7 @@ server {
ssl_protocols TLSv1.2; ssl_protocols TLSv1.2;
ssl_ciphers EECDH+AESGCM:EECDH+AES; ssl_ciphers EECDH+AESGCM:EECDH+AES;
ssl_ecdh_curve secp384r1; ssl_ecdh_curve prime256v1;
ssl_prefer_server_ciphers on; ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
@ -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 intall nodejs
sudo apt-get install nodejs
sudo npm install -g yarn sudo npm install -g yarn
## Redis ## Redis

View file

@ -13,5 +13,6 @@ Some people have started working on apps for the Mastodon API. Here is a list of
|Albatross|iOS||[@goldie_ice@mastodon.social](https://mastodon.social/users/goldie_ice)| |Albatross|iOS||[@goldie_ice@mastodon.social](https://mastodon.social/users/goldie_ice)|
|Tooter|Chrome|<https://github.com/ineffyble/tooter>|[@effy@mastodon.social](https://mastodon.social/users/effy)| |Tooter|Chrome|<https://github.com/ineffyble/tooter>|[@effy@mastodon.social](https://mastodon.social/users/effy)|
|tootstream|CLI|<https://github.com/magicalraccoon/tootstream>|[@Raccoon@mastodon.social](https://mastodon.social/users/Raccoon)| |tootstream|CLI|<https://github.com/magicalraccoon/tootstream>|[@Raccoon@mastodon.social](https://mastodon.social/users/Raccoon)|
|HackerNewsBot|CLI|<https://github.com/raymestalez/mastodon-hnbot>|[@rayalez@hackertribe.io](https://hackertribe.io/users/rayalez)|
If you have a project like this, let me know so I can add it to the list! If you have a project like this, let me know so I can add it to the list!

View file

@ -7,6 +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|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|
@ -17,9 +18,11 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz)
| [gay.crime.team](https://gay.crime.team) |the place for doin' gay crime online (please don't actually do crime here)|No|No| | [gay.crime.team](https://gay.crime.team) |the place for doin' gay crime online (please don't actually do crime here)|No|No|
| [icosahedron.website](https://icosahedron.website/) |Icosahedron-themed (well, visually), open registration.|Yes|No| | [icosahedron.website](https://icosahedron.website/) |Icosahedron-themed (well, visually), open registration.|Yes|No|
| [memetastic.space](https://memetastic.space) |Memes|Yes|No| | [memetastic.space](https://memetastic.space) |Memes|Yes|No|
| [masto.razrnet.fr](https://masto.razrnet.fr) |Instance Française pour tout le monde ! Développeurs, gamers, etc...|Yes|No|
| [social.diskseven.com](https://social.diskseven.com) |Single user|No|Yes| | [social.diskseven.com](https://social.diskseven.com) |Single user|No|Yes|
| [social.gestaltzerfall.net](https://social.gestaltzerfall.net) |Single user|No|No| | [social.gestaltzerfall.net](https://social.gestaltzerfall.net) |Single user|No|No|
| [mastodon.xyz](https://mastodon.xyz) |N/A|Yes|Yes| | [mastodon.xyz](https://mastodon.xyz) |N/A|Yes|Yes|
| [mastodon.land](https://mastodon.land) |N/A|Yes|Yes|
| [mastodon.partipirate.org](https://mastodon.partipirate.org) |French Pirate Party Instance - Politics and stuff|Yes|No| | [mastodon.partipirate.org](https://mastodon.partipirate.org) |French Pirate Party Instance - Politics and stuff|Yes|No|
| [social.targaryen.house](https://social.targaryen.house) |Federates everywhere, quick updates.|Yes|Yes| | [social.targaryen.house](https://social.targaryen.house) |Federates everywhere, quick updates.|Yes|Yes|
| [masto.themimitoof.fr](https://masto.themimitoof.fr) |N/A|Yes|Yes| | [masto.themimitoof.fr](https://masto.themimitoof.fr) |N/A|Yes|Yes|
@ -33,8 +36,8 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz)
| [social.alex73630.xyz](https://social.alex73630.xyz) |Francophones|Yes|Yes| | [social.alex73630.xyz](https://social.alex73630.xyz) |Francophones|Yes|Yes|
| [oc.todon.fr](https://oc.todon.fr) |Modérée et principalement francophone, pas de tolérances pour misogynie/LGBTphobies/validisme/etc.|Yes|Yes| | [oc.todon.fr](https://oc.todon.fr) |Modérée et principalement francophone, pas de tolérances pour misogynie/LGBTphobies/validisme/etc.|Yes|Yes|
| [maly.io](https://maly.io) |N/A|Yes|No| | [maly.io](https://maly.io) |N/A|Yes|No|
| [social.lou.lt](https://social.lou.lt) |N/A|Yes|No| | [social.lou.lt](https://social.lou.lt) |Francophones|Yes|No|
| [mastodon.ninetailed.uk](https://mastodon.ninetailed.uk) |N/A|Yes|No| | [mastodon.ninetailed.uk](https://mastodon.ninetailed.uk) |Open registrations, furry-friendly, UK-based|Yes|No|
| [soc.louiz.org](https://soc.louiz.org) |"Coucou"|Yes|No| | [soc.louiz.org](https://soc.louiz.org) |"Coucou"|Yes|No|
| [7nw.eu](https://7nw.eu) |N/A|Yes|No| | [7nw.eu](https://7nw.eu) |N/A|Yes|No|
| [mastodon.gougere.fr](https://mastodon.gougere.fr)|N/A|Yes|No| | [mastodon.gougere.fr](https://mastodon.gougere.fr)|N/A|Yes|No|
@ -47,6 +50,32 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz)
| [status.dissidence.ovh](https://status.dissidence.ovh)|N/A|Yes|Yes| | [status.dissidence.ovh](https://status.dissidence.ovh)|N/A|Yes|Yes|
| [mastodon.cc](https://mastodon.cc)|Art|Yes|No| | [mastodon.cc](https://mastodon.cc)|Art|Yes|No|
| [mastodon.technology](https://mastodon.technology)|Open registrations, federates everywhere, for tech folks|Yes|No| | [mastodon.technology](https://mastodon.technology)|Open registrations, federates everywhere, for tech folks|Yes|No|
| [mastodon.systemlab.fr](https://mastodon.systemlab.fr/)|Le mastodon Français, informatique, jeux-vidéos, gaming et hébergement.|Yes|No| | [mastodon.systemlab.fr](https://mastodon.systemlab.fr/)|Le mastodon Français, informatique, jeux-vidéos, gaming et hébergement.|Yes|
| [mastodon.top](https://mastodon.top) |N/A|Yes|Yes|
| [niu.moe](https://niu.moe/)|:dolls: The most cutest node ever, FR/EN, anime and computer :balloon:|Yes|Yes|
| [im-in.space](https://im-in.space/)|SPAAAAACE! Probably with a lot of French people. (Invite-only, might randomly open registrations)|No|Yes|
| [social.bytestemplar.com](https://social.bytestemplar.com)|N/A|Yes|No|
| [digitalhumanities.club](http://www.digitalhumanities.club)|[Digital humanities](http://whatisdigitalhumanities.com) community; invitations will open once code of conduct drafted.|No|No
| [design.vu](https://design.vu)|— what's your design view‽|Yes|No|
| [masto.raildecake.fr](https://masto.raildecake.fr)|Hebergé chez un FAI associatif dans le sud de la france, grillons & pins en options|Yes|No|
| [good-dragon.com](https://good-dragon.com/)|Quick updates, Relaxed Moderation, Federates Everywhere, Furries|Yes|No|
| [rich.gop](https://rich.gop/)|Federates everywhere, Open registration, Privacy respected|Yes|Yes|
| [social.nowa.re](https://social.nowa.re)|Open Registration|Yes|No|
| [mastodon.ml](http://mastodon.ml) |A chill place to hangout and chat about anime, programming and movies.|Yes|Yes|
| [off-the-clock.us](https://off-the-clock.us/)|The work day is over.|Yes|No|
| [infinimatix.net](https://infinimatix.net)|Informatics|Yes|Yes|
| [social.0day.agency](https://social.0day.agency)|Infosec, Hacking, Fun (only protonmail)|Yes|Yes|
| [kagrumez.lerk.io](https://kagrumez.lerk.io)|Open registration. German end english.|Yes|No|
| [meow.social](https://meow.social)|A furry fandom focused instance|Yes|No|
| [neumastodon.com](https://neumastodon.com/)|Northeastern University Mastodon |Yes|No|
| [dancingbanana.party](https://dancingbanana.party)|La banane qui danse.|Yes|No|
| [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|
| [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.fun](https://mastodon.fun/)|Mastodon for everyone ! |Yes|Yes|
| [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|
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

@ -1,3 +1,3 @@
Fabricator(:media_attachment) do Fabricator(:media_attachment) do
account
end end

View file

@ -1,3 +1,4 @@
Fabricator(:status) do Fabricator(:status) do
account
text "Lorem ipsum dolor sit amet" text "Lorem ipsum dolor sit amet"
end end

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

@ -99,11 +99,75 @@ RSpec.describe Account, type: :model do
end end
describe '#favourited?' do describe '#favourited?' do
pending let(:original_status) do
author = Fabricate(:account, username: 'original')
Fabricate(:status, account: author)
end
context 'when the status is a reblog of another status' do
let(:original_reblog) do
author = Fabricate(:account, username: 'original_reblogger')
Fabricate(:status, reblog: original_status, account: author)
end
it 'is is true when this account has favourited it' do
Fabricate(:favourite, status: original_reblog, account: subject)
expect(subject.favourited?(original_status)).to eq true
end
it 'is false when this account has not favourited it' do
expect(subject.favourited?(original_status)).to eq false
end
end
context 'when the status is an original status' do
it 'is is true when this account has favourited it' do
Fabricate(:favourite, status: original_status, account: subject)
expect(subject.favourited?(original_status)).to eq true
end
it 'is false when this account has not favourited it' do
expect(subject.favourited?(original_status)).to eq false
end
end
end end
describe '#reblogged?' do describe '#reblogged?' do
pending let(:original_status) do
author = Fabricate(:account, username: 'original')
Fabricate(:status, account: author)
end
context 'when the status is a reblog of another status'do
let(:original_reblog) do
author = Fabricate(:account, username: 'original_reblogger')
Fabricate(:status, reblog: original_status, account: author)
end
it 'is true when this account has reblogged it' do
Fabricate(:status, reblog: original_reblog, account: subject)
expect(subject.reblogged?(original_reblog)).to eq true
end
it 'is false when this account has not reblogged it' do
expect(subject.reblogged?(original_reblog)).to eq false
end
end
context 'when the status is an original status' do
it 'is true when this account has reblogged it' do
Fabricate(:status, reblog: original_status, account: subject)
expect(subject.reblogged?(original_status)).to eq true
end
it 'is false when this account has not reblogged it' do
expect(subject.reblogged?(original_status)).to eq false
end
end
end end
describe '.find_local' do describe '.find_local' do

View file

@ -91,10 +91,31 @@ RSpec.describe Status, type: :model do
end end
describe '#reblogs_count' do describe '#reblogs_count' do
pending it 'is the number of reblogs' do
Fabricate(:status, account: bob, reblog: subject)
Fabricate(:status, account: alice, reblog: subject)
expect(subject.reblogs_count).to eq 2
end
end end
describe '#favourites_count' do describe '#favourites_count' do
pending it 'is the number of favorites' do
Fabricate(:favourite, account: bob, status: subject)
Fabricate(:favourite, account: alice, status: subject)
expect(subject.favourites_count).to eq 2
end
end
describe '#proper' do
it 'is itself for original statuses' do
expect(subject.proper).to eq subject
end
it 'is the source status for reblogs' do
subject.reblog = other
expect(subject.proper).to eq other
end
end end
end end

View file

@ -3,8 +3,168 @@ require 'rails_helper'
RSpec.describe PostStatusService do RSpec.describe PostStatusService do
subject { PostStatusService.new } subject { PostStatusService.new }
it 'creates a new status' it 'creates a new status' do
it 'creates a new response status' account = Fabricate(:account)
it 'processes mentions' text = "test status update"
it 'pings PuSH hubs'
status = subject.call(account, text)
expect(status).to be_persisted
expect(status.text).to eq text
end
it 'creates a new response status' do
in_reply_to_status = Fabricate(:status)
account = Fabricate(:account)
text = "test status update"
status = subject.call(account, text, in_reply_to_status)
expect(status).to be_persisted
expect(status.text).to eq text
expect(status.thread).to eq in_reply_to_status
end
it 'creates a sensitive status' do
status = create_status_with_options(sensitive: true)
expect(status).to be_persisted
expect(status).to be_sensitive
end
it 'creates a status with spoiler text' do
spoiler_text = "spoiler text"
status = create_status_with_options(spoiler_text: spoiler_text)
expect(status).to be_persisted
expect(status.spoiler_text).to eq spoiler_text
end
it 'creates a status with empty default spoiler text' do
status = create_status_with_options(spoiler_text: nil)
expect(status).to be_persisted
expect(status.spoiler_text).to eq ''
end
it 'creates a status with the given visibility' do
status = create_status_with_options(visibility: :private)
expect(status).to be_persisted
expect(status.visibility).to eq "private"
end
it 'creates a status for the given application' do
application = Fabricate(:application)
status = create_status_with_options(application: application)
expect(status).to be_persisted
expect(status.application).to eq application
end
it 'processes mentions' do
mention_service = double(:process_mentions_service)
allow(mention_service).to receive(:call)
allow(ProcessMentionsService).to receive(:new).and_return(mention_service)
account = Fabricate(:account)
status = subject.call(account, "test status update")
expect(ProcessMentionsService).to have_received(:new)
expect(mention_service).to have_received(:call).with(status)
end
it 'processes hashtags' do
hashtags_service = double(:process_hashtags_service)
allow(hashtags_service).to receive(:call)
allow(ProcessHashtagsService).to receive(:new).and_return(hashtags_service)
account = Fabricate(:account)
status = subject.call(account, "test status update")
expect(ProcessHashtagsService).to have_received(:new)
expect(hashtags_service).to have_received(:call).with(status)
end
it 'pings PuSH hubs' do
allow(DistributionWorker).to receive(:perform_async)
allow(Pubsubhubbub::DistributionWorker).to receive(:perform_async)
account = Fabricate(:account)
status = subject.call(account, "test status update")
expect(DistributionWorker).to have_received(:perform_async).with(status.id)
expect(Pubsubhubbub::DistributionWorker).
to have_received(:perform_async).with(status.stream_entry.id)
end
it 'crawls links' do
allow(LinkCrawlWorker).to receive(:perform_async)
account = Fabricate(:account)
status = subject.call(account, "test status update")
expect(LinkCrawlWorker).to have_received(:perform_async).with(status.id)
end
it 'attaches the given media to the created status' do
account = Fabricate(:account)
media = Fabricate(:media_attachment)
status = subject.call(
account,
"test status update",
nil,
media_ids: [media.id],
)
expect(media.reload.status).to eq status
end
it 'does not allow attaching more than 4 files' do
account = Fabricate(:account)
expect do
subject.call(
account,
"test status update",
nil,
media_ids: [
Fabricate(:media_attachment, account: account),
Fabricate(:media_attachment, account: account),
Fabricate(:media_attachment, account: account),
Fabricate(:media_attachment, account: account),
Fabricate(:media_attachment, account: account),
].map(&:id),
)
end.to raise_error(
Mastodon::ValidationError,
I18n.t('media_attachments.validations.too_many'),
)
end
it 'does not allow attaching both videos and images' do
account = Fabricate(:account)
expect do
subject.call(
account,
"test status update",
nil,
media_ids: [
Fabricate(:media_attachment, type: :video, account: account),
Fabricate(:media_attachment, type: :image, account: account),
].map(&:id),
)
end.to raise_error(
Mastodon::ValidationError,
I18n.t('media_attachments.validations.images_and_video'),
)
end
def create_status_with_options(options = {})
subject.call(Fabricate(:account), "test", nil, options)
end
end end