Compare commits

..

1 commit

Author SHA1 Message Date
Eugen Rochko c370e8026b Make file attachment on MediaAttachment optional
Create MediaAttachment but without actual file download when domain is blocked with reject_media set to true
Clean up old media files when creating a new domain block with reject_media set to true
Return remote_url in media attachments API if local file is not present
Undo domain block action in admin UI
Ability to enable reject_media from admin UI
2017-04-15 21:19:17 +02:00
100 changed files with 476 additions and 1417 deletions

View file

@ -1,6 +1,12 @@
engines:
duplication:
enabled: false
enabled: true
exclude_paths:
- app/assets/javascripts/components/locales/
config:
languages:
- ruby
- javascript
rubocop:
enabled: true
eslint:

View file

@ -14,7 +14,6 @@ addons:
postgresql: 9.4
rvm:
- 2.3.4
- 2.4.1
services:
@ -38,4 +37,3 @@ before_script:
script:
- bundle exec rspec
- npm test
- i18n-tasks unused

View file

@ -12,22 +12,20 @@ WORKDIR /mastodon
COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \
&& BUILD_DEPS=" \
RUN BUILD_DEPS=" \
postgresql-dev \
libxml2-dev \
libxslt-dev \
build-base" \
&& apk -U upgrade && apk add \
$BUILD_DEPS \
nodejs@edge \
nodejs-npm@edge \
nodejs \
libpq \
libxml2 \
libxslt \
ffmpeg \
file \
imagemagick@edge \
imagemagick \
&& npm install -g npm@3 && npm install -g yarn \
&& bundle install --deployment --without test development \
&& yarn --ignore-optional \

View file

@ -1,13 +1,14 @@
# frozen_string_literal: true
source 'https://rubygems.org'
ruby '>= 2.3.0', '< 2.5.0'
ruby '2.4.1'
gem 'pkg-config'
gem 'rails', '~> 5.0.2'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.1.0'
gem 'jquery-rails'
gem 'puma'
@ -21,7 +22,6 @@ gem 'best_in_place', '~> 3.0.1'
gem 'paperclip', '~> 5.1'
gem 'paperclip-av-transcoder'
gem 'aws-sdk', '>= 2.0'
gem 'fog'
gem 'addressable'
gem 'devise'
@ -38,7 +38,7 @@ gem 'kaminari'
gem 'link_header'
gem 'nokogiri'
gem 'oj'
gem 'ostatus2', '~> 1.1'
gem 'ostatus2'
gem 'ox'
gem 'rabl'
gem 'rack-attack'
@ -57,7 +57,6 @@ gem 'sprockets-rails', :require => 'sprockets/railtie'
gem 'statsd-instrument'
gem 'twitter-text'
gem 'tzinfo-data'
gem 'whatlanguage'
gem 'react-rails'
gem 'browserify-rails'
@ -90,7 +89,7 @@ group :development do
gem 'bullet'
gem 'active_record_query_trace'
gem 'capistrano', '3.8.0'
gem 'capistrano'
gem 'capistrano-rails'
gem 'capistrano-rbenv'
gem 'capistrano-yarn'

View file

@ -1,7 +1,6 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (2.3.5)
actioncable (5.0.2)
actionpack (= 5.0.2)
nio4r (>= 1.2, < 3.0)
@ -42,7 +41,7 @@ GEM
tzinfo (~> 1.1)
addressable (2.5.1)
public_suffix (~> 2.0, >= 2.0.2)
airbrussh (1.2.0)
airbrussh (1.1.2)
sshkit (>= 1.6.1, != 1.7.0)
arel (7.1.4)
ast (2.3.0)
@ -112,6 +111,13 @@ GEM
cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0)
coderay (1.1.1)
coffee-rails (4.1.1)
coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.1.x)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.12.2)
colorize (0.8.1)
concurrent-ruby (1.0.5)
connection_pool (2.2.1)
@ -146,142 +152,13 @@ GEM
thread_safe
encryptor (3.0.0)
erubis (2.7.0)
excon (0.55.0)
execjs (2.7.0)
fabrication (2.16.1)
faker (1.7.3)
i18n (~> 0.5)
fast_blank (1.0.0)
fission (0.5.0)
CFPropertyList (~> 2.2)
fog (1.38.0)
fog-aliyun (>= 0.1.0)
fog-atmos
fog-aws (>= 0.6.0)
fog-brightbox (~> 0.4)
fog-cloudatcost (~> 0.1.0)
fog-core (~> 1.32)
fog-dynect (~> 0.0.2)
fog-ecloud (~> 0.1)
fog-google (<= 0.1.0)
fog-json
fog-local
fog-openstack
fog-powerdns (>= 0.1.1)
fog-profitbricks
fog-rackspace
fog-radosgw (>= 0.0.2)
fog-riakcs
fog-sakuracloud (>= 0.0.4)
fog-serverlove
fog-softlayer
fog-storm_on_demand
fog-terremark
fog-vmfusion
fog-voxel
fog-vsphere (>= 0.4.0)
fog-xenserver
fog-xml (~> 0.1.1)
ipaddress (~> 0.5)
fog-aliyun (0.1.0)
fog-core (~> 1.27)
fog-json (~> 1.0)
ipaddress (~> 0.8)
xml-simple (~> 1.1)
fog-atmos (0.1.0)
fog-core
fog-xml
fog-aws (1.3.0)
fog-core (~> 1.38)
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
fog-brightbox (0.11.0)
fog-core (~> 1.22)
fog-json
inflecto (~> 0.0.2)
fog-cloudatcost (0.1.2)
fog-core (~> 1.36)
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
fog-core (1.43.0)
builder
excon (~> 0.49)
formatador (~> 0.2)
fog-dynect (0.0.3)
fog-core
fog-json
fog-xml
fog-ecloud (0.3.0)
fog-core
fog-xml
fog-google (0.1.0)
fog-core
fog-json
fog-xml
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
fog-local (0.3.1)
fog-core (~> 1.27)
fog-openstack (0.1.20)
fog-core (>= 1.40)
fog-json (>= 1.0)
ipaddress (>= 0.8)
fog-powerdns (0.1.1)
fog-core (~> 1.27)
fog-json (~> 1.0)
fog-xml (~> 0.1)
fog-profitbricks (3.0.0)
fog-core (~> 1.42)
fog-json (~> 1.0)
fog-rackspace (0.1.4)
fog-core (>= 1.35)
fog-json (>= 1.0)
fog-xml (>= 0.1)
ipaddress (>= 0.8)
fog-radosgw (0.0.5)
fog-core (>= 1.21.0)
fog-json
fog-xml (>= 0.0.1)
fog-riakcs (0.1.0)
fog-core
fog-json
fog-xml
fog-sakuracloud (1.7.5)
fog-core
fog-json
fog-serverlove (0.1.2)
fog-core
fog-json
fog-softlayer (1.1.4)
fog-core
fog-json
fog-storm_on_demand (0.1.1)
fog-core
fog-json
fog-terremark (0.1.0)
fog-core
fog-xml
fog-vmfusion (0.1.0)
fission
fog-core
fog-voxel (0.1.0)
fog-core
fog-xml
fog-vsphere (1.9.1)
fog-core
rbvmomi (~> 1.9)
fog-xenserver (0.3.0)
fog-core
fog-xml
fog-xml (0.1.3)
fog-core
nokogiri (>= 1.5.11, < 2.0.0)
font-awesome-rails (4.7.0.1)
railties (>= 3.2, < 5.1)
formatador (0.2.5)
fuubar (2.2.0)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
@ -327,8 +204,6 @@ GEM
parser (>= 2.2.3.0)
rainbow (~> 2.2)
terminal-table (>= 1.5.1)
inflecto (0.0.2)
ipaddress (0.8.3)
jmespath (1.3.1)
jquery-rails (4.3.1)
rails-dom-testing (>= 1, < 3)
@ -375,7 +250,6 @@ GEM
mimemagic (0.3.2)
mini_portile2 (2.1.0)
minitest (5.10.1)
multi_json (1.12.1)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (4.1.0)
@ -383,13 +257,11 @@ GEM
nokogiri (1.7.1)
mini_portile2 (~> 2.1.0)
oj (2.18.5)
openssl (2.0.3)
orm_adapter (0.5.0)
ostatus2 (1.1.0)
ostatus2 (1.0.2)
addressable (~> 2.4)
http (~> 2.0)
nokogiri (~> 1.6)
openssl (~> 2.0)
ox (2.4.11)
paperclip (5.1.0)
activemodel (>= 4.2.0)
@ -465,11 +337,6 @@ GEM
thor (>= 0.18.1, < 2.0)
rainbow (2.2.1)
rake (12.0.0)
rbvmomi (1.11.0)
builder (~> 3.0)
json (>= 1.8)
nokogiri (~> 1.5)
trollop (~> 2.1)
react-rails (1.11.0)
babel-transpiler (>= 0.7.0)
connection_pool
@ -571,7 +438,6 @@ GEM
thread (0.2.2)
thread_safe (0.3.6)
tilt (2.0.7)
trollop (2.1.2)
twitter-text (1.14.5)
unf (~> 0.1.0)
tzinfo (1.2.3)
@ -582,7 +448,7 @@ GEM
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.3)
unf_ext (0.0.7.2)
unicode-display_width (1.1.3)
uniform_notifier (1.10.0)
warden (1.2.7)
@ -594,8 +460,6 @@ GEM
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
whatlanguage (1.0.6)
xml-simple (1.1.5)
xpath (2.0.0)
nokogiri (~> 1.3)
@ -612,12 +476,13 @@ DEPENDENCIES
binding_of_caller
browserify-rails
bullet
capistrano (= 3.8.0)
capistrano
capistrano-faster-assets (~> 1.0)
capistrano-rails
capistrano-rbenv
capistrano-yarn
capybara
coffee-rails (~> 4.1.0)
devise
devise-two-factor
doorkeeper
@ -625,7 +490,6 @@ DEPENDENCIES
fabrication
faker
fast_blank
fog
font-awesome-rails
fuubar
goldfinger
@ -645,7 +509,7 @@ DEPENDENCIES
microformats2
nokogiri
oj
ostatus2 (~> 1.1)
ostatus2
ox
paperclip (~> 5.1)
paperclip-av-transcoder
@ -683,7 +547,6 @@ DEPENDENCIES
tzinfo-data
uglifier (>= 1.3.0)
webmock
whatlanguage
RUBY VERSION
ruby 2.4.1p111

View file

@ -3,4 +3,3 @@
* * * *
- [ ] I searched or browsed the repos other issues to ensure this is not a duplicate.
- [ ] This bug happens on a [tagged release](https://github.com/tootsuite/mastodon/releases) and not on `master` (If you're a user, don't worry about this).

View file

@ -48,14 +48,6 @@ If you would like, you can [support the development of this project on Patreon][
- **Deployable via Docker**
You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
## Checking out
If you want a stable release for production use, you should use tagged releases. To checkout the latest available tagged version:
git clone https://github.com/tootsuite/mastodon.git
cd mastodon
git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
## Configuration
- `LOCAL_DOMAIN` should be the domain/hostname of your instance. This is **absolutely required** as it is used for generating unique IDs for everything federation-related

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View file

@ -1,14 +0,0 @@
import { openModal } from './modal';
import { changeSetting, saveSettings } from './settings';
export function showOnboardingOnce() {
return (dispatch, getState) => {
const alreadySeen = getState().getIn(['settings', 'onboarded']);
if (!alreadySeen) {
dispatch(openModal('ONBOARDING'));
dispatch(changeSetting(['onboarded'], true));
dispatch(saveSettings());
}
};
};

View file

@ -98,7 +98,7 @@ const StatusActionBar = React.createClass({
return (
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
<div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
<div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
<div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private' || status.get('visibility') === 'direct'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div style={{ float: 'left', marginRight: '18px'}}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>

View file

@ -7,8 +7,7 @@ import { isIOS } from '../is_mobile';
const messages = defineMessages({
toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' },
expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' },
expand_video: { id: 'video_player.video_error', defaultMessage: 'Video could not be played' }
expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' }
});
const videoStyle = {
@ -31,7 +30,7 @@ const muteStyle = {
zIndex: '5'
};
const coverStyle = {
const spoilerStyle = {
marginTop: '8px',
textAlign: 'center',
height: '100%',
@ -95,8 +94,7 @@ const VideoPlayer = React.createClass({
visible: !this.props.sensitive,
preview: true,
muted: true,
hasAudio: true,
videoError: false
hasAudio: true
};
},
@ -144,17 +142,12 @@ const VideoPlayer = React.createClass({
}
},
handleVideoError () {
this.setState({ videoError: true });
},
componentDidMount () {
if (!this.video) {
return;
}
this.video.addEventListener('loadeddata', this.handleLoadedData);
this.video.addEventListener('error', this.handleVideoError);
},
componentDidUpdate () {
@ -163,7 +156,6 @@ const VideoPlayer = React.createClass({
}
this.video.addEventListener('loadeddata', this.handleLoadedData);
this.video.addEventListener('error', this.handleVideoError);
},
componentWillUnmount () {
@ -172,7 +164,6 @@ const VideoPlayer = React.createClass({
}
this.video.removeEventListener('loadeddata', this.handleLoadedData);
this.video.removeEventListener('error', this.handleVideoError);
},
render () {
@ -203,7 +194,7 @@ const VideoPlayer = React.createClass({
if (!this.state.visible) {
if (sensitive) {
return (
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
<div role='button' tabIndex='0' style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton}
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
@ -211,7 +202,7 @@ const VideoPlayer = React.createClass({
);
} else {
return (
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
<div role='button' tabIndex='0' style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton}
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
@ -229,14 +220,6 @@ const VideoPlayer = React.createClass({
);
}
if (this.state.videoError) {
return (
<div style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='video-error-cover' >
<span style={spoilerSpanStyle}><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span>
</div>
);
}
return (
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
{spoilerButton}

View file

@ -8,7 +8,6 @@ import {
connectTimeline,
disconnectTimeline
} from '../actions/timelines';
import { showOnboardingOnce } from '../actions/onboarding';
import { updateNotifications, refreshNotifications } from '../actions/notifications';
import createBrowserHistory from 'history/lib/createBrowserHistory';
import {
@ -135,8 +134,6 @@ const Mastodon = React.createClass({
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
Notification.requestPermission();
}
store.dispatch(showOnboardingOnce());
},
componentWillUnmount () {

View file

@ -77,7 +77,7 @@ const ActionBar = React.createClass({
return (
<div className='detailed-status__action-bar'>
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton disabled={status.get('visibility') === 'direct' || status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><DropdownMenu size={18} icon='ellipsis-h' items={menu} direction="left" /></div>

View file

@ -1,13 +1,11 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import MediaModal from './media_modal';
import OnboardingModal from './onboarding_modal';
import VideoModal from './video_modal';
import BoostModal from './boost_modal';
import { TransitionMotion, spring } from 'react-motion';
const MODAL_COMPONENTS = {
'MEDIA': MediaModal,
'ONBOARDING': OnboardingModal,
'VIDEO': VideoModal,
'BOOST': BoostModal
};

View file

@ -1,251 +0,0 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Permalink from '../../../components/permalink';
import { TransitionMotion, spring } from 'react-motion';
import ComposeForm from '../../compose/components/compose_form';
import Search from '../../compose/components/search';
import NavigationBar from '../../compose/components/navigation_bar';
import ColumnHeader from './column_header';
import Immutable from 'immutable';
const messages = defineMessages({
home_title: { id: 'column.home', defaultMessage: 'Home' },
notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' },
local_title: { id: 'column.community', defaultMessage: 'Local timeline' },
federated_title: { id: 'column.public', defaultMessage: 'Federated timeline' }
});
const PageOne = ({ acct, domain }) => (
<div className='onboarding-modal__page onboarding-modal__page-one'>
<div style={{ flex: '0 0 auto' }}>
<div className='onboarding-modal__page-one__elephant-friend' />
</div>
<div>
<h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to Mastodon!' /></h1>
<p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='Mastodon is a social network that belongs to everyone.' /></p>
<p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, one of many independent Mastodon instances. Your full handle is {handle}' values={{ domain, handle: <strong>{acct}@{domain}</strong> }}/></p>
</div>
</div>
);
PageOne.propTypes = {
acct: React.PropTypes.string.isRequired,
domain: React.PropTypes.string.isRequired
};
const PageTwo = () => (
<div className='onboarding-modal__page onboarding-modal__page-two'>
<div className='figure non-interactive'>
<ComposeForm
text='Awoo! #introductions'
suggestions={Immutable.List()}
mentionedDomains={[]}
onChange={() => {}}
onSubmit={() => {}}
onPaste={() => {}}
onPickEmoji={() => {}}
onChangeSpoilerText={() => {}}
onClearSuggestions={() => {}}
onFetchSuggestions={() => {}}
onSuggestionSelected={() => {}}
/>
</div>
<p><FormattedMessage id='onboarding.page_two.compose' defaultMessage='Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.' /></p>
</div>
);
const PageThree = ({ me, domain }) => (
<div className='onboarding-modal__page onboarding-modal__page-three'>
<div className='figure non-interactive'>
<Search
value=''
onChange={() => {}}
onSubmit={() => {}}
onClear={() => {}}
onShow={() => {}}
/>
<div className='pseudo-drawer'>
<NavigationBar account={me} />
</div>
</div>
<p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/timelines/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/timelines/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }}/></p>
<p><FormattedMessage id='onboarding.page_three.profile' defaultMessage='Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.' /></p>
</div>
);
PageThree.propTypes = {
me: ImmutablePropTypes.map.isRequired,
domain: React.PropTypes.string.isRequired
};
const PageFour = ({ domain, intl }) => (
<div className='onboarding-modal__page onboarding-modal__page-four'>
<div className='onboarding-modal__page-four__columns'>
<div className='row'>
<div>
<div className='figure non-interactive'><ColumnHeader icon='home' type={intl.formatMessage(messages.home_title)} /></div>
<p><FormattedMessage id='onboarding.page_four.home' defaultMessage='Home timeline shows posts from people you follow'/></p>
</div>
<div>
<div className='figure non-interactive'><ColumnHeader icon='bell' type={intl.formatMessage(messages.notifications_title)} /></div>
<p><FormattedMessage id='onboarding.page_four.notifications' defaultMessage='Notifications show when someone interacts with you' /></p>
</div>
</div>
<div className='row'>
<div>
<div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='globe' type={intl.formatMessage(messages.federated_title)} /></div>
</div>
<div>
<div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='users' type={intl.formatMessage(messages.local_title)} /></div>
</div>
</div>
<p><FormattedMessage id='onboarding.page_five.public_timelines' defaultMessage='Federated timeline lists public posts from everyone who people on {domain} follow. Local timeline is the same, but limited to people on {domain}.' values={{ domain }} /></p>
</div>
</div>
);
PageFour.propTypes = {
domain: React.PropTypes.string.isRequired,
intl: React.PropTypes.object.isRequired
};
const PageSix = ({ admin }) => {
let adminSection = '';
if (admin) {
adminSection = (
<p>
<FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/accounts/${admin.get('id')}`}>@{admin.get('acct')}</Permalink> }} />
<br />
<FormattedMessage id='onboarding.page_six.read_guidelines' defaultMessage='Please, do not forget to read the {guidelines}!' values={{ guidelines: <a href='/about/more' target='_blank'><FormattedMessage id='onboarding.page_six.guidelines' defaultMessage='community guidelines' /></a> }}/>
</p>
);
}
return (
<div className='onboarding-modal__page onboarding-modal__page-six'>
<h1><FormattedMessage id='onboarding.page_six.almost_done' defaultMessage='Almost done...' /></h1>
{adminSection}
<p><FormattedMessage id='onboarding.page_six.github' defaultMessage='Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ github: <a href='https://github.com/tootsuite/mastodon' target='_blank' rel='noopener'>GitHub</a> }} /></p>
<p><FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms. And now... Bon Appetoot!' values={{ apps: <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_six.various_app' defaultMessage='various mobile apps' /></a> }} /></p>
</div>
);
};
PageSix.propTypes = {
admin: ImmutablePropTypes.map
};
const mapStateToProps = state => ({
me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
domain: state.getIn(['meta', 'domain'])
});
const OnboardingModal = React.createClass({
propTypes: {
onClose: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired,
me: ImmutablePropTypes.map.isRequired,
domain: React.PropTypes.string.isRequired,
admin: ImmutablePropTypes.map
},
getInitialState () {
return {
currentIndex: 0
};
},
mixins: [PureRenderMixin],
handleSkip (e) {
e.preventDefault();
this.props.onClose();
},
handleDot (i, e) {
e.preventDefault();
this.setState({ currentIndex: i });
},
handleNext (maxNum, e) {
e.preventDefault();
if (this.state.currentIndex < maxNum - 1) {
this.setState({ currentIndex: this.state.currentIndex + 1 });
} else {
this.props.onClose();
}
},
render () {
const { me, admin, domain, intl } = this.props;
const pages = [
<PageOne acct={me.get('acct')} domain={domain} />,
<PageTwo />,
<PageThree me={me} domain={domain} />,
<PageFour domain={domain} intl={intl} />,
<PageSix admin={admin} />
];
const { currentIndex } = this.state;
const hasMore = currentIndex < pages.length - 1;
let nextOrDoneBtn;
if(hasMore) {
nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__next'><FormattedMessage id='onboarding.next' defaultMessage='Next' /></a>;
} else {
nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__done'><FormattedMessage id='onboarding.next' defaultMessage='Done' /></a>;
}
const styles = pages.map((page, i) => ({
key: i,
style: { opacity: spring(i === currentIndex ? 1 : 0) }
}));
return (
<div className='modal-root__modal onboarding-modal'>
<TransitionMotion styles={styles}>
{interpolatedStyles =>
<div className='onboarding-modal__pager'>
{pages.map((page, i) =>
<div key={i} style={{ opacity: interpolatedStyles[i].style.opacity, pointerEvents: i === currentIndex ? 'auto' : 'none' }}>{page}</div>
)}
</div>
}
</TransitionMotion>
<div className='onboarding-modal__paginator'>
<div>
<a href='#' className='onboarding-modal__skip' onClick={this.handleSkip}><FormattedMessage id='onboarding.skip' defaultMessage='Skip' /></a>
</div>
<div className='onboarding-modal__dots'>
{pages.map((_, i) => <div key={i} onClick={this.handleDot.bind(null, i)} className={`onboarding-modal__dot ${i === currentIndex ? 'active' : ''}`} />)}
</div>
<div>
{nextOrDoneBtn}
</div>
</div>
</div>
);
}
});
export default connect(mapStateToProps)(injectIntl(OnboardingModal));

View file

@ -24,10 +24,8 @@ const makeGetStatusIds = () => createSelector([
if (columnSettings.getIn(['regex', 'body'], '').trim().length > 0) {
try {
if (showStatus) {
const regex = new RegExp(columnSettings.getIn(['regex', 'body']).trim(), 'i');
showStatus = !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'unescaped_content']) : statusForId.get('unescaped_content'));
}
const regex = new RegExp(columnSettings.getIn(['regex', 'body']).trim(), 'i');
showStatus = showStatus && !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'content']) : statusForId.get('content'));
} catch(e) {
// Bad regex, don't affect filters
}

View file

@ -74,7 +74,6 @@ const en = {
"navigation_bar.public_timeline": "Federated timeline",
"notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you",
"notification.mention": "{name} mentioned you",
"notification.reblog": "{name} boosted your status",
"notifications.clear_confirmation": "Are you sure you want to clear all your notifications?",
"notifications.clear": "Clear notifications",
@ -129,7 +128,6 @@ const en = {
"video_player.toggle_sound": "Toggle sound",
"video_player.toggle_visible": "Toggle visibility",
"video_player.expand": "Expand video",
"video_player.video_error": "Video could not be played",
};
export default en;

View file

@ -75,7 +75,6 @@ const fr = {
"navigation_bar.favourites": "Favoris",
"navigation_bar.info": "Plus d'informations",
"navigation_bar.logout": "Déconnexion",
"navigation_bar.mutes": "Utilisateurs muets",
"navigation_bar.follow_requests": "Demandes de suivi",
"reply_indicator.cancel": "Annuler",
"search.placeholder": "Rechercher",

View file

@ -1,125 +1,121 @@
const ja = {
"account.block": "@{name} さんをブロック",
"account.disclaimer": "このユーザーは他のインスタンスに所属しているため、数字が正確で無い場合があります。",
"account.edit_profile": "プロフィールを編集",
"account.follow": "フォロー",
"account.followers": "フォロワー",
"account.follows": "フォロー",
"account.follows_you": "フォローされています",
"column_back_button.label": "戻る",
"lightbox.close": "閉じる",
"loading_indicator.label": "読み込み中...",
"status.mention": "@{name} さんへの返信",
"status.delete": "削除",
"status.reply": "返信",
"status.reblog": "ブースト",
"status.favourite": "お気に入り",
"status.reblogged_by": "{name} さんにブーストされました",
"status.sensitive_warning": "不適切なコンテンツ",
"status.sensitive_toggle": "クリックして表示",
"status.show_more": "もっと見る",
"status.load_more": "もっと見る",
"status.show_less": "隠す",
"status.open": "Expand this status",
"status.report": "@{name} さんを通報",
"status.media_hidden": "非表示のメデイア",
"video_player.toggle_sound": "音の切り替え",
"account.mention": "@{name} さんに返信",
"account.mute": "ミュート",
"account.posts": "投稿",
"account.report": "@{name}を通報する",
"account.requested": "承認待ち",
"account.edit_profile": "プロフィールを編集",
"account.unblock": "@{name} さんのブロックを解除",
"account.unfollow": "フォロー解除",
"account.block": "@{name} さんをブロック",
"account.mute": "ミュート",
"account.unmute": "ミュート解除",
"boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
"column.blocks": "ブロックしたユーザー",
"column.community": "ローカルタイムライン",
"column.favourites": "お気に入り",
"column.follow_requests": "フォローリクエスト",
"account.follow": "フォロー",
"account.report": "@{name}を通報する",
"account.posts": "投稿",
"account.follows": "フォロー",
"account.followers": "フォロワー",
"account.follows_you": "フォローされています",
"account.requested": "承認待ち",
"follow_request.authorize": "許可",
"follow_request.reject": "拒否",
"getting_started.heading": "スタート",
"getting_started.about_addressing": "ドメインとユーザー名を知っているなら検索フォームに入力すればフォローできます。",
"getting_started.about_shortcuts": "対象のアカウントがあなたと同じドメインのユーザーならばユーザー名のみで検索できます。これは返信のときも一緒です。",
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub{github})から開発に参加したり、問題を報告したりできます。 {apps}",
"getting_started.apps": "さまざまなアプリで利用できます。",
"column.home": "ホーム",
"column.mutes": "ミュートしたユーザー",
"column.notifications": "通知",
"column.community": "ローカルタイムライン",
"column.public": "連合タイムライン",
"column_back_button.label": "戻る",
"column.notifications": "通知",
"column.favourites": "お気に入り",
"tabs_bar.compose": "投稿",
"tabs_bar.home": "ホーム",
"tabs_bar.mentions": "返信",
"tabs_bar.local_timeline": "ローカル",
"tabs_bar.federated_timeline": "連合",
"tabs_bar.notifications": "通知",
"compose_form.placeholder": "今なにしてる?",
"compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先のユーザーat {domains})に公開されます。{domainsCount, plural, one {that server} other {those servers}}を信頼しますか投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 もし{domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}ならばあなたの投稿のプライバシーは保護されず、ブーストされたり予期しないユーザーに見られる可能性があります。",
"compose_form.publish": "トゥート",
"compose_form.sensitive": "メディアを不適切なコンテンツとしてマークする",
"compose_form.spoiler": "テキストを隠す",
"compose_form.spoiler_placeholder": "閲覧注意",
"emoji_button.label": "絵文字を追加",
"empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
"empty_column.hashtag": "このハッシュタグはまだ使われていません。",
"compose_form.spoiler_placeholder": "内容注意メッセージ",
"compose_form.private": "非公開にする",
"compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先のユーザーat {domains})に公開されます。{domainsCount, plural, one {that server} other {those servers}}を信頼しますか投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 もし{domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}ならばあなたの投稿のプライバシーは保護されず、ブーストされたり予期しないユーザーに見られる可能性があります。",
"compose_form.unlisted": "公開タイムラインに表示しない",
"privacy.public.short": "公開",
"privacy.public.long": "公開TLに投稿する",
"privacy.unlisted.short": "未収載",
"privacy.unlisted.long": "公開TLで表示しない",
"privacy.private.short": "非公開",
"privacy.private.long": "フォロワーだけに公開",
"privacy.direct.short": "ダイレクト",
"privacy.direct.long": "含んだユーザーだけに公開",
"privacy.change": "投稿のプライバシーを変更",
"report.heading": "新規通報",
"report.placeholder": "コメント",
"report.target": "問題のユーザー",
"report.submit": "通報する",
"navigation_bar.edit_profile": "プロフィールを編集",
"navigation_bar.preferences": "ユーザー設定",
"navigation_bar.community_timeline": "ローカルタイムライン",
"navigation_bar.public_timeline": "連合タイムライン",
"navigation_bar.logout": "ログアウト",
"navigation_bar.favourites": "お気に入り",
"navigation_bar.blocks": "ブロックしたユーザー",
"navigation_bar.info": "サーバー情報",
"reply_indicator.cancel": "キャンセル",
"search.placeholder": "検索",
"search.account": "アカウント",
"search.hashtag": "ハッシュタグ",
"search.status_by": "{uuuname}からの投稿",
"search_results.total": "{count} 件",
"upload_area.title": "ファイルをこちらにドラッグしてください",
"upload_button.label": "メディアを追加",
"upload_form.undo": "やり直す",
"notification.follow": "{name} さんにフォローされました",
"notification.favourite": "{name} さんがあなたのトゥートをお気に入りに登録しました",
"notification.reblog": "{name} さんがあなたのトゥートをブーストしました",
"notification.mention": "{name} さんがあなたに返信しました",
"notifications.clear": "通知を片付ける",
"notifications.clear_confirmation": "通知を全部片付けます。大丈夫ですか?",
"notifications.column_settings.alert": "デスクトップ通知",
"notifications.column_settings.show": "カラムに表示",
"notifications.column_settings.follow": "新しいフォロワー",
"notifications.column_settings.favourite": "お気に入り",
"notifications.column_settings.mention": "返信",
"notifications.column_settings.reblog": "ブースト",
"notifications.column_settings.sound": "通知音を再生",
"empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
"empty_column.home.public_timeline": "連合タイムライン",
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
"empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
"follow_request.authorize": "許可",
"follow_request.reject": "拒否",
"getting_started.apps": "さまざまなアプリで利用できます。",
"getting_started.heading": "スタート",
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub{github})から開発に参加したり、問題を報告したりできます。 {apps}",
"home.column_settings.advanced": "上級者向け",
"empty_column.hashtag": "このハッシュタグはまだ使っていません。",
"upload_progress.label": "アップロード中…",
"emoji_button.label": "絵文字を追加",
"home.column_settings.basic": "シンプル",
"home.column_settings.filter_regex": "正規表現でフィルター",
"home.column_settings.advanced": "エキスパート",
"home.column_settings.show_reblogs": "ブースト表示",
"home.column_settings.show_replies": "返信表示",
"home.column_settings.filter_regex": "正規表現でフィルター",
"home.settings": "カラム設定",
"lightbox.close": "閉じる",
"loading_indicator.label": "読み込み中...",
"media_gallery.toggle_visible": "表示切り替え",
"missing_indicator.label": "見つかりません",
"navigation_bar.blocks": "ブロックしたユーザー",
"navigation_bar.community_timeline": "ローカルタイムライン",
"navigation_bar.edit_profile": "プロフィールを編集",
"navigation_bar.favourites": "お気に入り",
"navigation_bar.follow_requests": "フォローリクエスト",
"navigation_bar.info": "サーバー情報",
"navigation_bar.logout": "ログアウト",
"navigation_bar.mutes": "ミュートしたユーザー",
"navigation_bar.preferences": "ユーザー設定",
"navigation_bar.public_timeline": "連合タイムライン",
"notification.favourite": "{name} さんがあなたのトゥートをお気に入りに登録しました",
"notification.follow": "{name} さんにフォローされました",
"notification.mention": "{name} さんがあなたに返信しました",
"notification.reblog": "{name} さんがあなたのトゥートをブーストしました",
"notifications.clear": "通知を消去",
"notifications.clear_confirmation": "本当に通知を消去しますか?",
"notifications.column_settings.alert": "デスクトップ通知",
"notifications.column_settings.favourite": "お気に入り",
"notifications.column_settings.follow": "新しいフォロワー",
"notifications.column_settings.mention": "返信",
"notifications.column_settings.reblog": "ブースト",
"notifications.column_settings.show": "カラムに表示",
"notifications.column_settings.sound": "通知音を再生",
"notifications.settings": "カラム設定",
"privacy.change": "投稿のプライバシーを変更",
"privacy.direct.long": "メンションしたユーザーだけに公開",
"privacy.direct.short": "ダイレクト",
"privacy.private.long": "フォロワーだけに公開",
"privacy.private.short": "非公開",
"privacy.public.long": "公開TLに投稿する",
"privacy.public.short": "公開",
"privacy.unlisted.long": "公開TLで表示しない",
"privacy.unlisted.short": "未収載",
"reply_indicator.cancel": "キャンセル",
"report.heading": "新規通報",
"report.placeholder": "コメント",
"report.submit": "通報する",
"report.target": "問題のユーザー",
"search.placeholder": "検索",
"search.status_by": "{name}からの投稿",
"search_results.total": "{count} {count, plural, one {result} other {results}} 件",
"status.delete": "削除",
"status.favourite": "お気に入り",
"status.load_more": "もっと見る",
"status.media_hidden": "非表示のメデイア",
"status.mention": "@{name} さんへの返信",
"status.open": "詳細を表示",
"status.reblog": "ブースト",
"status.reblogged_by": "{name} さんにブーストされました",
"status.reply": "返信",
"status.report": "@{name} さんを通報",
"status.sensitive_toggle": "クリックして表示",
"status.sensitive_warning": "不適切なコンテンツ",
"status.show_less": "隠す",
"status.show_more": "もっと見る",
"tabs_bar.compose": "投稿",
"tabs_bar.federated_timeline": "連合",
"tabs_bar.home": "ホーム",
"tabs_bar.local_timeline": "ローカル",
"tabs_bar.notifications": "通知",
"upload_area.title": "ドラッグ&ドロップでアップロード",
"upload_button.label": "メディアを追加",
"upload_form.undo": "やり直す",
"upload_progress.label": "アップロード中…",
"video_player.expand": "動画の詳細",
"video_player.toggle_sound": "音の切り替え",
"video_player.toggle_visible": "表示切り替え",
"video_player.video_error": "動画の再生に失敗しました",
"missing_indicator.label": "見つかりません",
"boost_modal.combo": "次は{combo}を押せば、これをスキップできます。"
};
export default ja;

View file

@ -3,8 +3,6 @@ import { STORE_HYDRATE } from '../actions/store';
import Immutable from 'immutable';
const initialState = Immutable.Map({
onboarded: false,
home: Immutable.Map({
shows: Immutable.Map({
reblog: true,

View file

@ -48,9 +48,6 @@ const normalizeStatus = (state, status) => {
normalStatus.reblog = status.reblog.id;
}
const linebreakComplemented = status.content.replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n');
normalStatus.unescaped_content = new DOMParser().parseFromString(linebreakComplemented, 'text/html').documentElement.textContent;
return state.update(status.id, Immutable.Map(), map => map.mergeDeep(Immutable.fromJS(normalStatus)));
};

View file

@ -889,11 +889,6 @@ a.status__content__spoiler-link {
padding-right: 10px;
}
}
.column {
flex-grow: 1;
max-width: 600px; // This is just a guess at a sane max value
}
}
@media screen and (min-width: 2560px) {
@ -907,11 +902,6 @@ a.status__content__spoiler-link {
height: 90vh;
margin-top: 5vh;
}
.column {
flex-grow: 1;
max-width: 600px; // This is just a guess at a sane max value
}
}
.drawer__pager {
@ -942,12 +932,6 @@ a.status__content__spoiler-link {
}
}
.pseudo-drawer {
background: lighten($color1, 13%);
font-size: 13px;
text-align: left;
}
.drawer__header {
flex: 0 0 auto;
font-size: 16px;
@ -1219,10 +1203,6 @@ a.status__content__spoiler-link {
&:focus {
outline: 0;
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
.spoiler-input__input {
@ -1287,10 +1267,6 @@ a.status__content__spoiler-link {
color: $color5;
border-bottom-color: $color4;
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
@import 'boost';
@ -1407,7 +1383,7 @@ button.icon-button.active i.fa-retweet {
}
}
.media-spoiler, .video-error-cover {
.media-spoiler {
background: $color8;
color: $color5;
}
@ -1930,10 +1906,6 @@ button.icon-button.active i.fa-retweet {
&:focus {
background: lighten($color1, 4%);
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
.search__icon {
@ -2034,7 +2006,6 @@ button.icon-button.active i.fa-retweet {
.modal-root__modal {
pointer-events: auto;
display: flex;
z-index: 9999;
}
.media-modal {
@ -2048,237 +2019,6 @@ button.icon-button.active i.fa-retweet {
}
}
.onboarding-modal {
background: $color2;
color: $color1;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.onboarding-modal__pager {
height: 80vh;
width: 80vw;
max-width: 500px;
max-height: 350px;
position: relative;
& > div {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 25px;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
display: flex;
opacity: 0;
user-select: text;
}
}
@media screen and (max-width: 550px) {
.onboarding-modal {
width: 100%;
height: 100%;
border-radius: 0;
}
.onboarding-modal__pager {
width: 100%;
height: auto;
max-width: none;
max-height: none;
flex: 1 1 auto;
}
}
.onboarding-modal__paginator {
flex: 0 0 auto;
background: darken($color2, 8%);
display: flex;
padding: 25px;
& > div {
min-width: 33px;
}
a {
color: darken($color2, 34%);
text-decoration: none;
font-size: 14px;
font-weight: 500;
&:hover, &:focus, &:active {
color: darken($color2, 38%);
}
&.onboarding-modal__done, &.onboarding-modal__next {
color: $color4;
}
}
}
.onboarding-modal__dots {
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: center;
}
.onboarding-modal__dot {
width: 14px;
height: 14px;
border-radius: 14px;
background: darken($color2, 16%);
margin: 0 3px;
cursor: pointer;
&:hover {
background: darken($color2, 18%);
}
&.active {
cursor: default;
background: darken($color2, 24%);
}
}
.onboarding-modal__page {
cursor: default;
line-height: 21px;
h1 {
font-size: 18px;
font-weight: 500;
color: $color1;
margin-bottom: 20px;
}
a {
color: $color4;
&:hover, &:focus, &:active {
color: lighten($color4, 4%);
}
}
p {
font-size: 16px;
color: lighten($color1, 8%);
margin-top: 10px;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
strong {
font-weight: 500;
background: $color1;
color: $color2;
border-radius: 4px;
font-size: 14px;
padding: 3px 6px;
}
}
}
.onboarding-modal__page-one {
display: flex;
}
.onboarding-modal__page-one__elephant-friend {
background: image-url('elephant-friend.png') no-repeat 0 0;
width: 147px;
height: 160px;
margin-right: 10px;
}
.onboarding-modal__page-two,
.onboarding-modal__page-three,
.onboarding-modal__page-four,
.onboarding-modal__page-five {
p {
text-align: left;
}
.figure {
background: darken($color1, 8%);
color: $color2;
margin-bottom: 20px;
border-radius: 4px;
padding: 10px;
text-align: center;
font-size: 14px;
box-shadow: 1px 2px 6px rgba($color8, 0.3);
.onboarding-modal__image {
border-radius: 4px;
margin-bottom: 10px;
}
&.non-interactive {
pointer-events: none;
text-align: left;
}
}
}
.onboarding-modal__page-four__columns {
.row {
display: flex;
margin-bottom: 20px;
& > div {
flex: 1 1 0;
margin: 0 10px;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
p {
text-align: center;
}
}
&:last-child {
margin-bottom: 0;
}
}
.column-header {
color: $color5;
}
}
.onboarding-modal__image {
border-radius: 8px;
width: 70vw;
max-width: 450px;
max-height: auto;
display: block;
margin: auto;
margin-bottom: 20px;
}
.onboard-sliders {
display: inline-block;
max-width: 30px;
max-height: auto;
margin-left: 10px;
}
.boost-modal {
background: lighten($color2, 8%);
color: $color1;

View file

@ -8,5 +8,10 @@
}
.recovery-codes {
list-style: none;
column-count: 2;
height: 100px;
li {
list-style: decimal;
margin-left: 20px;
}
}

16
app/assets/stylesheets/variables.scss Executable file → Normal file
View file

@ -1,8 +1,8 @@
$color1: #282c37 !default; // darkest
$color2: #d9e1e8 !default; // lightest
$color3: #9baec8 !default; // lighter
$color4: #2b90d9 !default; // vibrant
$color5: #ffffff !default; // white
$color6: #df405a !default; // error red
$color7: #79bd9a !default; // succ green
$color8: #000000 !default; // black
$color1: #282c37; // darkest
$color2: #d9e1e8; // lightest
$color3: #9baec8; // lighter
$color4: #2b90d9; // vibrant
$color5: #ffffff; // white
$color6: #df405a; // error red
$color7: #79bd9a; // succ green
$color8: #000000; // black

View file

@ -15,7 +15,7 @@ module Admin
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_block.created_msg')
else
render action: :new
end
@ -28,7 +28,7 @@ module Admin
def destroy
@domain_block = DomainBlock.find(params[:id])
UnblockDomainService.new.call(@domain_block, resource_params[:retroactive])
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_block.destroyed_msg')
end
private

View file

@ -14,7 +14,7 @@ class Api::OEmbedController < ApiController
def stream_entry_from_url(url)
params = Rails.application.routes.recognize_path(url)
raise ActiveRecord::RecordNotFound unless params[:controller] == 'stream_entries' && params[:action] == 'show'
raise ActiveRecord::NotFound unless params[:controller] == 'stream_entries' && params[:action] == 'show'
StreamEntry.find(params[:id])
end

View file

@ -7,7 +7,6 @@ class HomeController < ApplicationController
@body_classes = 'app-body'
@token = find_or_create_access_token.token
@web_settings = Web::Setting.find_by(user: current_user)&.data || {}
@admin = Account.find_local(Setting.site_contact_username)
@streaming_api_base_url = Rails.configuration.x.streaming_api_base_url
end

View file

@ -4,8 +4,4 @@ module ApplicationHelper
def active_nav_class(path)
current_page?(path) ? 'active' : ''
end
def show_landing_strip?
!user_signed_in? && !single_user_mode?
end
end

View file

@ -1,19 +0,0 @@
# frozen_string_literal: true
module StyleHelper
def stylesheet_for_layout
if asset_exist? 'custom.css'
'custom'
else
'application'
end
end
def asset_exist?(path)
if Rails.configuration.assets.compile
Rails.application.precompiled_assets.include? path
else
Rails.application.assets_manifest.assets[path].present?
end
end
end

View file

@ -3,8 +3,6 @@
class AtomSerializer
include RoutingHelper
INVALID_XML_CHARS = /[^\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]/
class << self
def render(element)
document = Ox::Document.new(version: '1.0')
@ -41,7 +39,7 @@ class AtomSerializer
add_namespaces(feed)
append_element(feed, 'id', account_url(account, format: 'atom'))
append_element(feed, 'title', account.display_name.presence || account.username)
append_element(feed, 'title', account.display_name)
append_element(feed, 'subtitle', account.note)
append_element(feed, 'updated', account.updated_at.iso8601)
append_element(feed, 'logo', full_asset_url(account.avatar.url(:original)))
@ -313,15 +311,11 @@ class AtomSerializer
def append_element(parent, name, content = nil, attributes = {})
element = Ox::Element.new(name)
attributes.each { |k, v| element[k] = sanitize_str(v) }
element << sanitize_str(content) unless content.nil?
attributes.each { |k, v| element[k] = v.to_s }
element << content.to_s unless content.nil?
parent << element
end
def sanitize_str(raw_str)
raw_str.to_s.gsub(INVALID_XML_CHARS, '')
end
def add_namespaces(parent)
parent['xmlns'] = TagManager::XMLNS
parent['xmlns:thr'] = TagManager::THR_XMLNS
@ -333,8 +327,8 @@ class AtomSerializer
end
def serialize_status_attributes(entry, status)
append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text?
append_element(entry, 'content', Formatter.instance.format(status.proper).to_str, type: 'html', 'xml:lang': status.language)
append_element(entry, 'summary', status.spoiler_text) if status.spoiler_text?
append_element(entry, 'content', Formatter.instance.format(status.proper).to_str, type: 'html')
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))

View file

@ -59,12 +59,7 @@ class NotificationMailer < ApplicationMailer
return if @notifications.empty?
I18n.with_locale(@me.user.locale || I18n.default_locale) do
mail to: @me.user.email,
subject: I18n.t(
:subject,
scope: [:notification_mailer, :digest],
count: @notifications.size
)
mail to: @me.user.email, subject: I18n.t('notification_mailer.digest.subject', count: @notifications.size)
end
end
end

View file

View file

@ -7,9 +7,6 @@ class DomainBlock < ApplicationRecord
validates :domain, presence: true, uniqueness: true
has_many :accounts, foreign_key: :domain, primary_key: :domain
delegate :count, to: :accounts, prefix: true
def self.blocked?(domain)
where(domain: domain, severity: :suspend).exists?
end

View file

@ -1,15 +1,13 @@
# frozen_string_literal: true
class Import < ApplicationRecord
FILE_TYPES = ['text/plain', 'text/csv'].freeze
self.inheritance_column = false
belongs_to :account, required: true
enum type: [:following, :blocking, :muting]
validates :type, presence: true
belongs_to :account
FILE_TYPES = ['text/plain', 'text/csv'].freeze
has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV['PAPERCLIP_SECRET']
validates_attachment_content_type :data, content_type: FILE_TYPES

View file

@ -110,10 +110,6 @@ class Status < ApplicationRecord
results
end
def non_sensitive_with_media?
!sensitive? && media_attachments.any?
end
class << self
def as_home_timeline(account)
where(account: [account] + account.following)

View file

@ -16,7 +16,7 @@ class FollowRemoteAccountService < BaseService
return Account.find_local(username) if TagManager.instance.local_domain?(domain)
account = Account.find_remote(username, domain)
return account unless account_needs_webfinger_update?(account)
return account unless account&.last_webfingered_at.nil? || 1.day.from_now(account.last_webfingered_at) < Time.now.utc
Rails.logger.debug "Looking up webfinger for #{uri}"
@ -62,10 +62,6 @@ class FollowRemoteAccountService < BaseService
private
def account_needs_webfinger_update?(account)
account&.last_webfingered_at.nil? || account.last_webfingered_at <= 1.day.ago
end
def get_feed(url)
response = http_client.get(Addressable::URI.parse(url))
[response.to_s, Nokogiri::XML(response)]

View file

@ -6,7 +6,7 @@ class NotifyService < BaseService
@activity = activity
@notification = Notification.new(account: @recipient, activity: @activity)
return if recipient.user.nil? || blocked?
return if blocked? || recipient.user.nil?
create_notification
send_email if email_enabled?

View file

@ -19,7 +19,6 @@ class PostStatusService < BaseService
sensitive: options[:sensitive],
spoiler_text: options[:spoiler_text] || '',
visibility: options[:visibility],
language: detect_language(text),
application: options[:application])
attach_media(status, media)
@ -52,10 +51,6 @@ class PostStatusService < BaseService
media.update(status_id: status.id)
end
def detect_language(text)
WhatLanguage.new(:all).language_iso(text) || 'en'
end
def process_mentions_service
@process_mentions_service ||= ProcessMentionsService.new
end

View file

@ -119,7 +119,6 @@ class ProcessFeedService < BaseService
spoiler_text: content_warning(entry),
created_at: published(entry),
reply: thread?(entry),
language: content_language(entry),
visibility: visibility_scope(entry)
)
@ -162,7 +161,13 @@ class ProcessFeedService < BaseService
xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each do |link|
next if [TagManager::TYPES[:group], TagManager::TYPES[:collection]].include? link['ostatus:object-type']
mentioned_account = account_from_href(link['href'])
url = Addressable::URI.parse(link['href'])
mentioned_account = if TagManager.instance.web_domain?(url.host)
Account.find_local(url.path.gsub('/users/', ''))
else
Account.find_by(url: link['href']) || FetchRemoteAccountService.new.call(link['href'])
end
next if mentioned_account.nil? || processed_account_ids.include?(mentioned_account.id)
@ -173,16 +178,6 @@ class ProcessFeedService < BaseService
end
end
def account_from_href(href)
url = Addressable::URI.parse(href)
if TagManager.instance.web_domain?(url.host)
Account.find_local(url.path.gsub('/users/', ''))
else
Account.find_by(uri: href) || Account.find_by(url: href) || FetchRemoteAccountService.new.call(href)
end
end
def hashtags_from_xml(parent, xml)
tags = xml.xpath('./xmlns:category', xmlns: TagManager::XMLNS).map { |category| category['term'] }.select(&:present?)
ProcessHashtagsService.new.call(parent, tags)
@ -239,10 +234,6 @@ class ProcessFeedService < BaseService
xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS).content
end
def content_language(xml = @xml)
xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS)['xml:lang']&.presence || 'en'
end
def content_warning(xml = @xml)
xml.at_xpath('./xmlns:summary', xmlns: TagManager::XMLNS)&.content || ''
end

View file

@ -6,7 +6,6 @@ class UnfollowService < BaseService
# @param [Account] target_account Which to unfollow
def call(source_account, target_account)
follow = source_account.unfollow!(target_account)
return unless follow
NotificationWorker.perform_async(build_xml(follow), source_account.id, target_account.id) unless target_account.local?
UnmergeWorker.perform_async(target_account.id, source_account.id)
end

View file

@ -71,6 +71,6 @@
%p Dette dokumentet er lisensiert under CC-BY-SA. De ble sist oppdatert 12. april 2017.
%p
Dokumentet er en adoptert og endret versjon fra
Dokumentet er en adoptert og endret versjon fra
= succeed '.' do
= link_to 'Discourse privacy policy', 'https://github.com/discourse/discourse'

View file

@ -1,34 +1,34 @@
.card.h-card.p-author{ style: "background-image: url(#{account.header.url( :original)})" }
- if user_signed_in? && current_account.id != account.id && !current_account.requested?(account)
.card.h-card.p-author{ style: "background-image: url(#{@account.header.url( :original)})" }
- if user_signed_in? && current_account.id != @account.id && !current_account.requested?(@account)
.controls
- if current_account.following?(account)
= link_to t('accounts.unfollow'), unfollow_account_path(account), data: { method: :post }, class: 'button'
- if current_account.following?(@account)
= link_to t('accounts.unfollow'), unfollow_account_path(@account), data: { method: :post }, class: 'button'
- else
= link_to t('accounts.follow'), follow_account_path(account), data: { method: :post }, class: 'button'
= link_to t('accounts.follow'), follow_account_path(@account), data: { method: :post }, class: 'button'
- elsif !user_signed_in?
.controls
.remote-follow
= link_to t('accounts.remote_follow'), account_remote_follow_path(account), class: 'button'
.avatar= image_tag account.avatar.url(:original), class: 'u-photo'
= link_to t('accounts.remote_follow'), account_remote_follow_path(@account), class: 'button'
.avatar= image_tag @account.avatar.url(:original), class: 'u-photo'
%h1.name
%span.p-name.emojify= display_name(account)
%span.p-name.emojify= display_name(@account)
%small
%span= "@#{account.username}"
= fa_icon('lock') if account.locked?
%span= "@#{@account.username}"
= fa_icon('lock') if @account.locked?
.details
.bio
.account__header__content.p-note.emojify= Formatter.instance.simplified_format(account)
.account__header__content.p-note.emojify= Formatter.instance.simplified_format(@account)
.details-counters
.counter{ class: active_nav_class(short_account_url(account)) }
= link_to short_account_url(account), class: 'u-url u-uid' do
.counter{ class: active_nav_class(short_account_url(@account)) }
= link_to short_account_url(@account), class: 'u-url u-uid' do
%span.counter-label= t('accounts.posts')
%span.counter-number= number_with_delimiter account.statuses_count
.counter{ class: active_nav_class(following_account_url(account)) }
= link_to following_account_url(account) do
%span.counter-number= number_with_delimiter @account.statuses_count
.counter{ class: active_nav_class(following_account_url(@account)) }
= link_to following_account_url(@account) do
%span.counter-label= t('accounts.following')
%span.counter-number= number_with_delimiter account.following_count
.counter{ class: active_nav_class(followers_account_url(account)) }
= link_to followers_account_url(account) do
%span.counter-number= number_with_delimiter @account.following_count
.counter{ class: active_nav_class(followers_account_url(@account)) }
= link_to followers_account_url(@account) do
%span.counter-label= t('accounts.followers')
%span.counter-number= number_with_delimiter account.followers_count
%span.counter-number= number_with_delimiter @account.followers_count

View file

@ -1,7 +1,7 @@
- content_for :page_title do
= t('accounts.people_who_follow', name: display_name(@account))
= render 'header', account: @account
= render partial: 'header'
.accounts-grid
- if @followers.empty?

View file

@ -1,7 +1,7 @@
- content_for :page_title do
= t('accounts.people_followed_by', name: display_name(@account))
= render 'header', account: @account
= render partial: 'header'
.accounts-grid
- if @following.empty?

View file

@ -14,13 +14,13 @@
%meta{ property: 'og:image:height', content: '120' }/
%meta{ property: 'twitter:card', content: 'summary' }/
- if show_landing_strip?
- if !user_signed_in? && !single_user_mode?
= render partial: 'shared/landing_strip', locals: { account: @account }
.h-feed
%data.p-name{ value: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/
= render 'header', account: @account
= render partial: 'header'
- if @statuses.empty?
.accounts-grid

View file

@ -61,9 +61,8 @@
= surround '(', ')' do
= number_to_human_size @account.media_attachments.sum('file_file_size')
- if @account.local?
%div{ style: 'float: right' }
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button'
%div{ style: 'float: right' }
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button'
%div{ style: 'float: left' }
- if @account.silenced?

View file

@ -1,24 +1,24 @@
- content_for :page_title do
= t('admin.domain_blocks.title')
= t('admin.domain_block.title')
%table.table
%thead
%tr
%th= t('admin.domain_blocks.domain')
%th= t('admin.domain_blocks.severity')
%th= t('admin.domain_blocks.reject_media')
%th= t('admin.domain_block.domain')
%th= t('admin.domain_block.severity')
%th= t('admin.domain_block.reject_media')
%th
%tbody
- @blocks.each do |block|
%tr
%td
%samp= block.domain
%td= t("admin.domain_blocks.severities.#{block.severity}")
%td= t("admin.domain_block.severities.#{block.severity}")
%td
- if block.reject_media? || block.suspend?
%i.fa.fa-check
%td
= table_link_to 'undo', t('admin.domain_blocks.undo'), admin_domain_block_path(block)
= table_link_to 'undo', t('admin.domain_block.undo'), admin_domain_block_path(block)
= paginate @blocks
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button'
= link_to t('admin.domain_block.add_new'), new_admin_domain_block_path, class: 'button'

View file

@ -1,17 +1,17 @@
- content_for :page_title do
= t('.title')
= t('admin.domain_block.new.title')
= simple_form_for @domain_block, url: admin_domain_blocks_path do |f|
= render 'shared/error_messages', object: @domain_block
%p.hint= t('.hint')
%p.hint= t('admin.domain_block.new.hint')
= f.input :domain, placeholder: t('admin.domain_blocks.domain')
= f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| t(".severity.#{type}") }
= f.input :domain, placeholder: t('admin.domain_block.domain')
= f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("admin.domain_block.new.severity.#{type}") }
%p.hint= t('.severity.desc_html')
%p.hint= t('admin.domain_block.new.severity.desc_html')
= f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_media'), hint: I18n.t('admin.domain_blocks.reject_media_hint')
= f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_block.reject_media'), hint: I18n.t('admin.domain_block.reject_media_hint')
.actions
= f.button :button, t('.create'), type: :submit
= f.button :button, t('admin.domain_block.new.create'), type: :submit

View file

@ -1,15 +1,9 @@
- content_for :page_title do
= t('admin.domain_blocks.show.title', domain: @domain_block.domain)
= t('admin.domain_block.show.title', domain: @domain_block.domain)
= simple_form_for @domain_block, url: admin_domain_block_path(@domain_block), method: :delete do |f|
= f.input :retroactive,
as: :boolean,
wrapper: :with_label,
label: t(".retroactive.#{@domain_block.severity}"),
hint: t(:affected_accounts,
scope: [:admin, :domain_blocks, :show],
count: @domain_block.accounts_count)
= f.input :retroactive, as: :boolean, wrapper: :with_label, label: I18n.t("admin.domain_block.show.retroactive.#{@domain_block.severity}"), hint: I18n.t('admin.domain_block.show.affected_accounts', count: Account.where(domain: @domain_block.domain).count)
.actions
= f.button :button, t('.undo'), type: :submit
= f.button :button, t('admin.domain_block.show.undo'), type: :submit

View file

@ -12,7 +12,7 @@
%p
%strong= t('admin.reports.comment.label')
\:
= @report.comment.presence || t('admin.reports.comment.none')
= @report.comment.presence || t('reports.comment.none')
- unless @report.statuses.empty?
%hr/

View file

@ -1,6 +1,6 @@
- content_for :header_tags do
%script#initial-state{:type => 'application/json'}!= json_escape(render(file: 'home/initial_state', formats: :json))
= javascript_include_tag 'application', integrity: true, crossorigin: 'anonymous'
= javascript_include_tag 'application', integrity: true
= react_component 'Mastodon', default_props, class: 'app-holder', prerender: false

View file

@ -5,9 +5,7 @@ node(:meta) do
streaming_api_base_url: @streaming_api_base_url,
access_token: @token,
locale: I18n.locale,
domain: Rails.configuration.x.local_domain,
me: current_account.id,
admin: @admin.try(:id),
boost_modal: current_account.user.setting_boost_modal,
}
end
@ -20,10 +18,9 @@ node(:compose) do
end
node(:accounts) do
store = {}
store[current_account.id] = partial('api/v1/accounts/show', object: current_account)
store[@admin.id] = partial('api/v1/accounts/show', object: @admin) unless @admin.nil?
store
{
current_account.id => partial('api/v1/accounts/show', object: current_account),
}
end
node(:settings) { @web_settings }

View file

@ -1,5 +1,5 @@
- content_for :header_tags do
= javascript_include_tag 'application_public', integrity: true, crossorigin: 'anonymous'
= javascript_include_tag 'application_public', integrity: true
- content_for :content do
.admin-wrapper

2
app/views/layouts/application.html.haml Executable file → Normal file
View file

@ -17,7 +17,7 @@
= ' - '
= site_title
= stylesheet_link_tag stylesheet_for_layout, media: 'all'
= stylesheet_link_tag 'application', media: 'all'
= csrf_meta_tags
= yield :header_tags

View file

@ -1,5 +1,5 @@
- content_for :header_tags do
= javascript_include_tag 'application_public', integrity: true, crossorigin: 'anonymous'
= javascript_include_tag 'application_public', integrity: true
- content_for :content do
.container

View file

@ -3,6 +3,6 @@
%head
%meta{:charset => 'utf-8'}/
= stylesheet_link_tag 'application', media: 'all'
= javascript_include_tag 'application_public', integrity: true, crossorigin: 'anonymous'
= javascript_include_tag 'application_public', integrity: true
%body.embed
= yield

View file

@ -1,5 +1,5 @@
- content_for :header_tags do
= javascript_include_tag 'application_public', integrity: true, crossorigin: 'anonymous'
= javascript_include_tag 'application_public', integrity: true
- content_for :content do
.container= yield

View file

@ -1,6 +1,7 @@
%p.hint= t('two_factor_auth.recovery_instructions')
%h3= t('two_factor_auth.recovery_codes')
%ol.recovery-codes
- recovery_codes.each do |code|
- @codes.each do |code|
%li
%samp= code

View file

@ -1,4 +1,4 @@
- content_for :page_title do
= t('settings.two_factor_auth')
= render partial: 'recovery_codes', object: @codes
= render 'recovery_codes'

View file

@ -1,4 +1,4 @@
- content_for :page_title do
= t('settings.two_factor_auth')
= render partial: 'recovery_codes', object: @codes
= render 'recovery_codes'

View file

@ -10,8 +10,6 @@
= link_to t('two_factor_auth.setup'), new_settings_two_factor_auth_path, class: 'block-button'
- if current_user.otp_required_for_login
%p
.simple_form
%p.hint= t('two_factor_auth.lost_recovery_codes')
= link_to t('two_factor_auth.generate_recovery_codes'), recovery_codes_settings_two_factor_auth_path, data: { method: 'POST' }, class: 'block-button'

View file

@ -1,4 +0,0 @@
- if activity.is_a?(Status) && activity.spoiler_text?
%meta{ property: 'og:description', content: activity.spoiler_text }/
- else
%meta{ property: 'og:description', content: activity.content }/

View file

@ -1,6 +0,0 @@
- if activity.is_a?(Status) && activity.non_sensitive_with_media?
%meta{ property: 'og:image', content: full_asset_url(activity.media_attachments.first.file.url(:small)) }/
- else
%meta{ property: 'og:image', content: full_asset_url(account.avatar.url(:original)) }/
%meta{ property: 'og:image:width', content: '120' }/
%meta{ property: 'og:image:height', content: '120' }/

View file

@ -6,12 +6,21 @@
%meta{ property: 'og:type', content: 'article' }/
%meta{ property: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/
= render 'stream_entries/og_description', activity: @stream_entry.activity
= render 'stream_entries/og_image', activity: @stream_entry.activity, account: @account
- if @stream_entry.activity.is_a?(Status) && !@stream_entry.activity.spoiler_text.blank?
%meta{ property: 'og:description', content: @stream_entry.activity.spoiler_text }/
- else
%meta{ property: 'og:description', content: @stream_entry.activity.content }/
- if @stream_entry.activity.is_a?(Status) && !@stream_entry.activity.sensitive? && @stream_entry.activity.media_attachments.size > 0
%meta{ property: 'og:image', content: full_asset_url(@stream_entry.activity.media_attachments.first.file.url(:small)) }/
- else
%meta{ property: 'og:image', content: full_asset_url(@account.avatar.url(:original)) }/
%meta{ property: 'og:image:width', content: '120' }/
%meta{ property: 'og:image:height', content: '120' }/
%meta{ property: 'twitter:card', content: 'summary' }/
- if show_landing_strip?
- if !user_signed_in? && !single_user_mode?
= render partial: 'shared/landing_strip', locals: { account: @stream_entry.account }
.activity-stream.activity-stream-headless.h-entry

View file

@ -1,17 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'rspec' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
require "rubygems"
require "bundler/setup"
load Gem.bin_path("rspec-core", "rspec")

View file

@ -1,11 +1,8 @@
# frozen_string_literal: true
lock '3.8.0'
set :repo_url, ENV.fetch('REPO', 'https://github.com/tootsuite/mastodon.git')
set :branch, ENV.fetch('BRANCH', 'master')
lock '3.7.2'
set :application, 'mastodon'
set :repo_url, 'https://github.com/tootsuite/mastodon.git'
set :branch, 'master'
set :rbenv_type, :user
set :rbenv_ruby, File.read('.ruby-version').strip
set :migration_role, :app

View file

@ -104,7 +104,6 @@ Rails.application.configure do
:authentication => ENV['SMTP_AUTH_METHOD'] || :plain,
:openssl_verify_mode => ENV['SMTP_OPENSSL_VERIFY_MODE'],
:enable_starttls_auto => ENV['SMTP_ENABLE_STARTTLS_AUTO'] || true,
:ca_file => "/etc/ssl/certs/ca-certificates.crt"
}
config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym

View file

@ -31,22 +31,8 @@ search:
- app/assets/fonts
- app/assets/videos
ignore_missing:
- 'activemodel.errors.*'
- 'activerecord.attributes.*'
- 'activerecord.errors.*'
- '{devise,pagination,doorkeeper}.*'
- '{datetime,time}.*'
- 'simple_form.{yes,no}'
- 'simple_form.{placeholders,hints,labels}.*'
- 'simple_form.{error_notification,required}.:'
- 'errors.messages.*'
- 'activerecord.errors.models.doorkeeper/*'
ignore_unused:
- 'activemodel.errors.*'
- 'activerecord.attributes.*'
- 'activerecord.errors.*'
- '{devise,pagination,doorkeeper}.*'
- '{datetime,time}.*'
- 'simple_form.{yes,no}'

3
config/initializers/assets.rb Executable file → Normal file
View file

@ -8,6 +8,5 @@ Rails.application.config.assets.version = '1.0'
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
Rails.application.config.assets.precompile += %w(application_public.js custom.css)
Rails.application.config.assets.precompile += %w(application_public.js)
Rails.application.config.assets.initialize_on_precompile = true

View file

@ -9,7 +9,7 @@ Rails.application.configure do
config.x.local_domain = host
config.x.web_domain = web_host
config.x.use_https = https
config.x.use_s3 = ENV['S3_ENABLED'] == 'true' || ENV['GCS_ENABLED'] == 'true'
config.x.use_s3 = ENV['S3_ENABLED'] == 'true'
config.action_mailer.default_url_options = { host: web_host, protocol: https ? 'https://' : 'http://', trailing_slash: false }
config.x.streaming_api_base_url = 'http://localhost:4000'

View file

@ -15,7 +15,7 @@ if ENV['S3_ENABLED'] == 'true'
Paperclip::Attachment.default_options[:url] = ':s3_domain_url'
Paperclip::Attachment.default_options[:s3_host_name] = ENV.fetch('S3_HOSTNAME') { "s3-#{ENV.fetch('S3_REGION')}.amazonaws.com" }
Paperclip::Attachment.default_options[:path] = '/:class/:attachment/:id_partition/:style/:filename'
Paperclip::Attachment.default_options[:s3_headers] = { 'Cache-Control' => 'max-age=315576000' }
Paperclip::Attachment.default_options[:s3_headers] = { 'Cache-Control' => 'max-age=315576000', 'Expires' => 10.years.from_now.httpdate }
Paperclip::Attachment.default_options[:s3_permissions] = 'public-read'
Paperclip::Attachment.default_options[:s3_region] = ENV.fetch('S3_REGION') { 'us-east-1' }
@ -42,23 +42,3 @@ else
Paperclip::Attachment.default_options[:path] = (ENV['PAPERCLIP_ROOT_PATH'] || ':rails_root/public/system') + '/:class/:attachment/:id_partition/:style/:filename'
Paperclip::Attachment.default_options[:url] = (ENV['PAPERCLIP_ROOT_URL'] || '/system') + '/:class/:attachment/:id_partition/:style/:filename'
end
if ENV['GCS_ENABLED'] == 'true'
Paperclip::Attachment.default_options.update({
:path => "images/:class/:id/:attachment/:style/img_:fingerprint",
:storage => :fog,
:fog_credentials => {
:provider => 'Google',
:google_storage_access_key_id => ENV.fetch('GCS_ACCESS_KEY_ID'),
:google_storage_secret_access_key => ENV.fetch('GCS_SECRET_ACCESS_KEY'),
:path_style => !ENV['GCS_ALIAS_HOST'].blank?
},
:fog_directory => ENV.fetch('GCS_BUCKET'),
:fog_public => true,
:fog_host => 'https://' + ENV.fetch('GCS_BUCKET'),
})
unless ENV['GCS_ALIAS_HOST'].blank?
Paperclip::Attachment.default_options[:url] = ENV['GCS_ALIAS_HOST']
end
end

View file

@ -160,6 +160,8 @@ bg:
disable: Деактивирай
enable: Активирай
instructions_html: "<strong>Сканирай този QR код с Google Authenticator или подобно приложение от своя телефон</strong>. Oтсега нататък, това приложение ще генерира код, който ще трябва да въвеждаш при всяко влизане."
plaintext_secret_html: 'Тайна в обикновен текст: <samp>%{secret}</samp>'
warning: Ако не можеш да настроиш приложението за удостверяване сега, избери "Деактивирай". В противен случай, няма да можеш да влезеш в акаунта си.
users:
invalid_email: E-mail адресът е невалиден
invalid_otp_token: Невалиден код

View file

@ -71,7 +71,6 @@ en:
profile_url: Profile URL
public: Public
push_subscription_expires: PuSH subscription expires
reset_password: Reset password
salmon_url: Salmon URL
silence: Silence
statuses: Statuses
@ -80,7 +79,7 @@ en:
undo_suspension: Undo suspension
username: Username
web: Web
domain_blocks:
domain_block:
add_new: Add new
created_msg: Domain block is now being processed
destroyed_msg: Domain block has been undone
@ -107,7 +106,6 @@ en:
silence: Unsilence all existing accounts from this domain
suspend: Unsuspend all existing accounts from this domain
title: Undo domain block for %{domain}
undo: Undo
title: Domain Blocks
undo: Undo
pubsubhubbub:
@ -260,6 +258,24 @@ en:
missing_resource: Could not find the required redirect URL for your account
proceed: Proceed to follow
prompt: 'You are going to follow:'
reports:
comment:
label: Comment
none: None
delete: Delete
id: ID
mark_as_resolved: Mark as resolved
report: 'Report #%{id}'
reported_account: Reported account
reported_by: Reported by
reports: Reports
resolved: Resolved
silence_account: Silence account
status: Status
suspend_account: Suspend account
target: Target
unresolved: Unresolved
view: View
settings:
authorized_apps: Authorized apps
back: Back to Mastodon
@ -294,9 +310,11 @@ en:
instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated.
manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
recovery_codes: Recovery Codes
recovery_codes_regenerated: Recovery codes successfully regenerated
recovery_instructions: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. Keep the recovery codes safe, for example by printing them and storing them with other important documents.
setup: Set up
warning: If you cannot configure an authenticator app right now, you should click "disable" or you won't be able to login.
wrong_code: The entered code was invalid! Are server time and device time correct?
users:
invalid_email: The e-mail address is invalid

View file

@ -145,7 +145,7 @@ eo:
unlisted: Publika, sed ne aperos en publikaj tempolinioj
stream_entries:
click_to_show: Alklaki por montri
reblogged: diskonigis
reblogged: diskonigita
sensitive_content: Tikla enhavo
time:
formats:
@ -155,6 +155,8 @@ eo:
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

View file

@ -160,6 +160,8 @@ es:
disable: Deshabilitar
enable: Habilitar
instructions_html: "<strong>Escanea este código QR desde Google Authenticator o una aplicación similar en su teléfono</strong>. Desde ahora, esta aplicación va a generar tokens que tienes que ingresar cuando quieras iniciar sesión."
plaintext_secret_html: 'Código en texto plano: <samp>%{secret}</samp>'
warning: Sí no puedes configurar una aplicación de autenticación ahora, deberás deshabilitar la autenticación de dos factores o no podrás iniciar sesión.
users:
invalid_email: La dirección de correo es incorrecta
invalid_otp_token: Código de dos factores incorrecto

View file

@ -155,6 +155,8 @@ fi:
disable: Poista käytöstä
enable: Ota käyttöön
instructions_html: "<strong>Skannaa tämä QR-koodi Google Authenticator- tai vastaavaan sovellukseen puhelimellasi</strong>. Tästä hetkestä lähtien ohjelma luo koodin, mikä sinun tarvitsee syöttää sisäänkirjautuessa."
plaintext_secret_html: 'Plain-text secret: <samp>%{secret}</samp>'
warning: Jos et juuri nyt voi konfiguroida authenticator-applikaatiota juuri nyt, sinun pitäisi klikata "Poista käytöstä" tai et voi kirjautua sisään.
users:
invalid_email: Virheellinen sähköposti
invalid_otp_token: Virheellinen kaksivaihetunnistuskoodi

View file

@ -79,7 +79,7 @@ fr:
undo_suspension: Annuler la suspension
username: Nom d'utilisateur
web: Web
domain_blocks:
domain_block:
add_new: Ajouter
domain: Domaine
new:
@ -241,6 +241,24 @@ fr:
missing_resource: L'URL de redirection n'a pas pu être trouvée
proceed: Continuez pour suivre
prompt: 'Vous allez suivre :'
reports:
comment:
label: Commentaire
none: Aucun
delete: Supprimer
id: ID
mark_as_resolved: Marqué comme résolu
report: 'Signalement #%{id}'
reported_account: Compte signalé
reported_by: Signalé par
reports: Signalements
resolved: Résolus
silence_account: Rendre le compte muet
status: Statut
suspend_account: Suspendre le compte
target: Cible
unresolved: Non résolus
view: Voir
settings:
authorized_apps: Applications autorisées
back: Retour vers Mastodon
@ -270,6 +288,8 @@ fr:
disable: Désactiver
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."
plaintext_secret_html: 'Code secret en clair : <samp>%{secret}</samp>'
warning: Si vous ne pouvez pas configurer une application d'authentification maintenant, vous devriez cliquer sur "Désactiver" pour ne pas bloquer l'accès à votre compte.
users:
invalid_email: L'adresse courriel est invalide
invalid_otp_token: Le code d'authentification à deux facteurs est invalide

View file

@ -156,6 +156,8 @@ hr:
disable: Onemogući
enable: Omogući
instructions_html: "<strong>Skeniraj ovaj QR kod into Google Authenticator or a similiar app on your phone</strong>. Od sada, ta aplikacija će generirati tokene koje ćeš unijeti pri prijavljivanju."
plaintext_secret_html: 'Plain-text secret: <samp>%{secret}</samp>'
warning: Ako trenuno ne možeš konfigurirati authenticator app, trebaš kliknuti "onemogući" ili se nećeš moći prijaviti.
users:
invalid_email: E-mail adresa nije valjana
invalid_otp_token: Nevaljani dvo-faktorski kod

View file

@ -165,6 +165,7 @@ it:
instructions_html: "<strong>Scannerizza questo QR code con Google Authenticator o un'app TOTP simile sul tuo telefono</strong>. Da ora in poi, quell'applicazione genererà codici da inserire necessariamente per eseguire l'accesso."
manual_instructions: 'Se non puoi scannerizzare il QR code e hai bisogno di inserirlo manualmente, questo è il codice segreto in chiaro:'
setup: Configura
warning: Se non puoi convalidare immediatamente la tua app di autenticazione, dovresti selezionare "disabilita" o non sarai più in grado di eseguire l'accesso.
wrong_code: Il codice inserito non è corretto! Assicurati che l'orario del server e l'orario del telefono siano corretti.
users:
invalid_email: L'indirizzo e-mail inserito non è valido

View file

@ -71,7 +71,6 @@ ja:
profile_url: プロフィールURL
public: パブリック
push_subscription_expires: PuSH購読期限切れ
reset_password: パスワード再設定
salmon_url: Salmon URL
silence: サイレンス
statuses: トゥート数
@ -80,10 +79,8 @@ ja:
undo_suspension: 停止から戻す
username: ユーザー名
web: Web
domain_blocks:
domain_block:
add_new: 新規追加
created_msg: ドメインブロック処理を完了しました
destroyed_msg: ドメインブロックを外しました
domain: ドメイン
new:
create: ブロックを作成
@ -93,21 +90,8 @@ ja:
silence: サイレンス
suspend: 停止
title: 新規ドメインブロック
reject_media: メディアファイルを拒否
reject_media_hint: ローカルに保村されたメディアファイルを削除し、今後のダウンロードを拒否します。停止とは無関係です。
severities:
silence: サイレンス
suspend: 停止
severity: 深刻度
show:
affected_accounts: "データベース中の%{count}個のアカウントに影響します"
retroactive:
silence: このドメインからの存在するすべてのアカウントのサイレンスを戻す
suspend: このドメインからの存在するすべてのアカウントの停止を戻す
title: "%{domain}のドメインブロックを戻す"
undo: 元に戻す
title: ドメインブロック
undo: 元に戻す
pubsubhubbub:
callback_url: コールバックURL
confirmed: 確認済み
@ -122,7 +106,7 @@ ja:
delete: 削除
id: ID
mark_as_resolved: 解決済みとしてマーク
report: レポート#%{id}
report: 'レポート#%{id}'
reported_account: 報告対象アカウント
reported_by: 報告者
resolved: 解決済み
@ -258,6 +242,24 @@ ja:
missing_resource: リダイレクト先が見つかりませんでした
proceed: フォローする
prompt: 'フォローしようとしています:'
reports:
comment:
label: コメント
none: なし
delete: 削除
id: ID
mark_as_resolved: 解決する
report: '通報 #%{id}'
reported_account: 通報されているユーザー
reported_by: 通報者
reports: 通報
resolved: 解決済み
silence_account: ユーザーをサイレンスする
status: 現状
suspend_account: ユーザーを停止する
target: 通報されているユーザー
unresolved: 未決
view: 見る
settings:
authorized_apps: 認証済みアプリ
back: 戻る
@ -288,13 +290,10 @@ ja:
disable: 無効
enable: 有効
enabled_success: 二段階認証が有効になりました
generate_recovery_codes: 復元コードを生成
instructions_html: "<strong>Google Authenticatorか、もしくはほかのTOTPアプリでこのQRコードをスキャンしてください。</strong>これ以降、ログインするときはそのアプリで生成されるコードが必要になります。"
lost_recovery_codes: リカバリコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリコードは無効になります。
manual_instructions: 'QRコードがスキャンできず、手動での登録を希望の場合はこのシークレットコードを利用してください。:'
recovery_codes_regenerated: リカバリーコードが再生成されました。
recovery_instructions: 携帯電話を紛失した場合、以下の内どれかのリカバリコードを使用してアカウントへアクセスすることができます。 リカバリコードは印刷して安全に保管してください。
setup: 初期設定
warning: 現在認証アプリを設定できない場合、無効に設定して、有効にしないでください。
wrong_code: コードが間違っています。サーバー上の時間とデバイス上の時間が一致していることを確認してください。
users:
invalid_email: メールアドレスが無効です

View file

@ -156,6 +156,8 @@ nl:
disable: Uitschakelen
enable: Inschakelen
instructions_html: "<strong>Scan deze QR-code in Google Authenticator of een soortgelijke app op je mobiele telefoon</strong>. Van nu af aan creëert deze app tokens die je bij aanmelden moet invoeren."
plaintext_secret_html: 'Gewone-tekst geheim: <samp>%{secret}</samp>'
warning: Als je nu geen authenticator-app kunt installeren, moet je "Uitschakelen" kiezen of je kunt niet meer aanmelden.
users:
invalid_email: Het e-mailadres is ongeldig
invalid_otp_token: Ongeldige twee-factorcode

View file

@ -155,6 +155,8 @@
disable: Skru av
enable: Skru på
instructions_html: "<strong>Scan denne QR-koden i Google Authenticator eller en lignende app på telefonen din</strong>. Fra nå av vil denne applikasjonen generere koder for deg som skal brukes under innlogging"
plaintext_secret_html: 'Plain-text secret: <samp>%{secret}</samp>'
warning: Hvis du ikke kan konfigurere en autentiseringsapp nå bør du trykke "Skru av"; ellers vil du ikke kunne logge inn.
users:
invalid_email: E-postaddressen er ugyldig
invalid_otp_token: Ugyldig tofaktorkode

View file

@ -155,6 +155,10 @@ pl:
disable: Wyłącz
enable: Włącz
instructions_html: "<strong>Zeskanuj ten kod QR na swoim urządzeniu za pomocą Google Authenticator, FreeOTP lub podobnej aplikacji</strong>. Od teraz będzie ona generowała kody wymagane przy logowaniu."
plaintext_secret_html: 'Sekret: <samp>%{secret}</samp>'
warning: Jeśli nie jesteś w stanie skonfigurować aplikacji uwierzytelniania dwustopniowego w tej chwili, wyłącz uwierzytelnianie dwustopniowe. W przeciwnym wypadku nie będziesz się w stanie zalogować!
users:
invalid_email: Adres e-mail jest niepoprawny
invalid_otp_token: Kod uwierzytelniający jest niepoprawny
will_paginate:
page_gap: "&hellip;"

View file

@ -1,174 +1,29 @@
---
pt:
about:
about_mastodon: Mastodon é um servidor de rede social <em>grátis, e open-source</em>. Uma alternativa <em>descentralizada</em> ás plataformas comerciais, que evita o risco de uma única empresa monopolizar a sua comunicação. Escolha um servidor que você confie &mdash; qualquer um que escolher, você poderá interagir com todo o resto. Qualquer um pode ter uma instância Mastodon e assim participar na <em>rede social federada</em> sem problemas.
about_this: Sobre essa instância
apps: Aplicações
business_email: 'Email comercial:'
closed_registrations: Registros estão fechadas para essa instância.
contact: Contato
description_headline: O que é %{domain}?
domain_count_after: outras instâncias
domain_count_before: Conectado a
features:
api: Aberto para API de aplicações e serviços
blocks: Bloqueos e ferramentas para mudar
characters: 500 caracteres por post
chronology: Timeline são cronologicas
ethics: 'Design ético: sem propaganda, sem tracking'
gifv: GIFV e vídeos curtos
privacy: Granular, privacidade setada por post
public: Timelines públicas
features_headline: O que torna Mastodon diferente
get_started: Comece aqui
links: Links
about_mastodon: Mastodon é um servidor de rede social <em>grátis, e open-source</em>. Uma alternativa <em>descentralizada</em> ás plataformas comerciais, que evita o risco de uma única empresa monopolizar a sua comunicação. Qualquer um pode ter uma instância Mastodon e assim participar na <em>rede social federada</em> sem problemas.
get_started: Como começar
source_code: Source code
other_instances: Outras instâncias
terms: Termos
user_count_after: usuários
user_count_before: Lugar de
accounts:
follow: Seguir
followers: Seguidores
following: Seguindo
following: Following
nothing_here: Não há nada aqui!
people_followed_by: Pessoas seguidas por %{name}
people_who_follow: Pessoas que seguem %{name}
posts: Posts
remote_follow: Acesso remoto
unfollow: Unfollow
admin:
accounts:
are_you_sure: Você tem certeza?
display_name: Nome mostrado
domain: Domain
edit: Editar
email: E-mail
feed_url: URL do Feed
followers: Seguidores
follows: Seguindo
location:
all: Todos
local: Local
remote: Remoto
title: Local
media_attachments: Mídia anexadas
moderation:
all: Todos
silenced: Silenciado
suspended: Supenso
title: Moderação
most_recent_activity: Atividade mais recente
most_recent_ip: IP mais recente
not_subscribed: Não inscrito
order:
alphabetic: Alfabética
most_recent: Mais recente
title: Ordem
perform_full_suspension: Fazer suspensão completa
profile_url: URL do perfil
public: Público
push_subscription_expires: PuSH subscription expires
salmon_url: Salmon URL
silence: Silêncio
statuses: Status
title: Contas
undo_silenced: Desfazer silenciar
undo_suspension: Desfazer supensão
username: Usuário
web: Web
domain_blocks:
add_new: Adicionar nova
created_msg: Bloqueio do domínio está sendo processado
destroyed_msg: Bloqueio de domínio está sendo desfeito
domain: Domínio
new:
create: Criar bloqueio
hint: O bloqueio de dominio não vai previnir a criação de entradas no banco de dados, mas irá, retroativamente e automaticamente aplicar métodos de moderação específica nessas contas.
severity:
desc_html: "<strong>Silenciar</strong> irá fazer com que os posts dessas contas sejam invisíveis para todos que não a seguem. <strong>Supender</strong> irá remover todos o conteúdo das contas, mídia e dados do perfil."
silence: Silenciar
suspend: Suspender
title: Novo bloqueio de domínio
reject_media: Rejeitar arquivos de mídia
reject_media_hint: Remove localmente arquivos armazenados e rejeita fazer o download de novos no futuro. Irrelevante em suspensões.
severities:
silence: Silenciar
suspend: Suspender
severity: Severidade
show:
affected_accounts:
one: Uma conta no banco de dados afetada
other: "%{count} contas no banco de dados afetada"
retroactive:
silence: Desilenciar todas as contas existentes nesse domínio
suspend: Desuspender todas as contas existentes nesse domínio
title: Desfazer bloqueio de domínio para %{domain}
title: Bloqueio de domínio
undo: Desfazer
pubsubhubbub:
callback_url: URL de Callback
confirmed: Confirmado
expires_in: Expira em
last_delivery: Última entrega
title: PubSubHubbub
topic: Tópico
reports:
comment:
label: Commentário
none: None
delete: Deletar
id: ID
mark_as_resolved: Marque como resolvido
report: 'Report #%{id}'
reported_account: Conta reportada
reported_by: Reportado por
resolved: Resolvido
silence_account: Conta silenciada
status: Status
suspend_account: Conta suspensa
target: Target
title: Reports
unresolved: Unresolved
view: View
settings:
click_to_edit: Clique para editar
contact_information:
email: Entre um endereço de email público
label: Informação de contato
username: Entre com usuário
registrations:
closed_message:
desc_html: Mostrar na página inicial quando registros estão fecados<br/>Você pode usar tags HTML
title: Mensagem de registro fechados
open:
disabled: Desabilitado
enabled: Habilitado
title: Aberto para registro
setting: Preferências
site_description:
desc_html: Mostrar como parágrafo e usado como meta tag.<br/>Vôce pode usar tags HTML, em particular <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.
title: Descrição do site
site_description_extended:
desc_html: Mostrar na página de informação extendiada <br/>Você pode usar tags HTML
title: Descrição extendida do site
site_title: Título do site
title: Preferências do site
title: Administração
application_mailer:
settings: 'Mudar preferências de email: %{link}'
signature: notificações Mastodon de %{instance}
view: 'View:'
applications:
invalid_url: URL dada é inválida
auth:
change_password: Mudar senha
change_password: Mudar password
didnt_get_confirmation: Não recebeu instruções de confirmação?
forgot_password: Esqueceu a senha?
forgot_password: Esqueceu a password?
login: Entrar
register: Registar
resend_confirmation: Reenviar instruções de confirmação
reset_password: Resetar senha
reset_password: Reset password
set_new_password: Editar password
generic:
changes_saved_msg: Mudanças guardadas!

View file

@ -158,7 +158,9 @@ ru:
enable: Включить
instructions_html: "<strong>Отсканируйте этот QR-код с помощью Google Authenticator или другого подобного приложения на Вашем телефоне</strong>. С этого момента приложение будет генерировать токены, которые будет необходимо ввести для входа."
manual_instructions: 'Если Вы не можете отсканировать QR-код и хотите ввести его вручную, секрет представлен здесь открытым текстом:'
plaintext_secret_html: 'Секрет открытым текстом: <samp>%{secret}</samp>'
setup: Настроить
warning: Если сейчас у Вас не получается настроить аутентификатор, нажмите "отключить", иначе Вы не сможете войти!
users:
invalid_email: Введенный e-mail неверен
invalid_otp_token: Введен неверный код

View file

@ -10,13 +10,11 @@ ja:
note: プロフィールは160文字まで設定することができます。
imports:
data: 他の Mastodon サーバーからエクスポートしたCSVファイルを選択して下さい
sessions:
otp: 携帯電話に表示された2段階認証コードを入力するか、生成したリカバリーコードを使用してください。
labels:
defaults:
avatar: アイコン
confirm_new_password: 新しいパスワード(確認用)
confirm_password: パスワード(確認用)
confirm_password: 新しいパスワード
current_password: 現在のパスワード
data: データ
display_name: 表示名
@ -24,13 +22,12 @@ ja:
header: ヘッダー
locale: 言語
locked: 非公開アカウントにする
new_password: 新しいパスワード
new_password: パスワード
note: プロフィール
otp_attempt: 二段階認証コード
password: パスワード
setting_boost_modal: ブーストする前に確認ダイアログを表示する
setting_default_privacy: 投稿の公開範囲
severity: 重大性
type: インポートする項目
username: ユーザー名
interactions:

View file

@ -4,17 +4,17 @@ pt:
labels:
defaults:
avatar: Avatar
confirm_new_password: Confirme nova senha
confirm_password: Confirme a senha
current_password: Senha atual
confirm_new_password: Confirme nova password
confirm_password: Confirme a password
current_password: Password atual
display_name: Nome
email: Endereço de email
header: Header
locale: Linguagem
new_password: Nova senha
new_password: Nova password
note: Biografia
password: Senha
username: Usuário
password: Password
username: Username
interactions:
must_be_follower: Bloquear notificações de não-seguidores
must_be_following: Bloquear notificações de pessoas que você

View file

@ -145,6 +145,8 @@ zh-CN:
disable: 禁用
enable: 启用
instructions_html: "<strong>使用 Google Authenticator 或类似 APP 扫描二维码</strong>。现在起APP 将会生成登陆时必须的两步验证码。"
plaintext_secret_html: 密钥: <samp>%{secret}</samp>
warning: 如果你现在没有 Google Authenticator 或类似授权 APP你应该先「禁用」本功能否则你将不能正常登陆。
users:
invalid_email: 无效的邮箱
invalid_otp_token: 无效的两步验证码

View file

@ -79,7 +79,7 @@ zh-HK:
undo_suspension: 解除停權
username: 用戶名稱
web: 用戶頁面
domain_blocks:
domain_block:
add_new: 新增
domain: 域名阻隔
new:
@ -246,6 +246,24 @@ zh-HK:
missing_resource: 無法找到你用戶的轉接網址
proceed: 下一步
prompt: 你希望關注︰
reports:
comment:
label: 詳細解釋
none: 沒有
delete: 刪除
id: ID
mark_as_resolved: 標示為「已處理」
report: '舉報 #%{id}'
reported_account: 舉報 account
reported_by: 舉報者
reports: 舉報
resolved: 已處埋
silence_account: 將用戶靜音
status: 狀態
suspend_account: 將用戶停權
target: 對像
unresolved: 未處埋
view: 檢視
settings:
authorized_apps: 授權應用程式
back: 回到 Mastodon
@ -279,7 +297,10 @@ zh-HK:
instructions_html: "<strong>請用你手機的認證器應用程式(如 Google Authenticator、Authy掃描這裏的QR 圖形碼</strong>。在雙重認證啟用後,你登入時將須要使用此應用程式產生的認證碼。"
manual_instructions: 如果你無法掃描 QR 圖形碼,請手動輸入這個文字密碼︰
setup: 設定
warning: 如果你現在無法正確設定你的應用程式,請即「停用」雙重認證,否則日後可能無法登入本站。
wrong_code: 你輸入的認證碼並不正確!可能伺服器時間和你手機不一致,請檢查你手機的時鐘,或與本站管理員聯絡。
users:
invalid_email: 電郵地址格式不正確
invalid_otp_token: 雙重認證確認碼不正確
will_paginate:
page_gap: "&hellip;"

View file

@ -79,7 +79,7 @@ zh-TW:
undo_suspension: 取消停權
username: 使用者名稱
web: Web
domain_blocks:
domain_block:
add_new: 新增
domain: 網域
new:
@ -240,6 +240,24 @@ zh-TW:
missing_resource: 無法找到資源
proceed: 下一步
prompt: '您希望關注︰'
reports:
comment:
label: 留言
none:
delete: 刪除
id: ID
mark_as_resolved: 標記為已解決
report: '檢舉 #%{id}'
reported_account: 被檢舉帳號
reported_by: 檢舉人
reports: 檢舉
resolved: 已解決
silence_account: 靜音帳號
status: 狀態
suspend_account: 停權帳號
target: 目標
unresolved: 未解決
view: 檢視
settings:
authorized_apps: 已授權應用程式
back: 回到 Mastodon
@ -273,7 +291,10 @@ zh-TW:
instructions_html: <strong>請用您手機的認證器應用程式(如 Google Authenticator、Authy掃描這裡的 QR 圖形碼</strong>。在雙因子認證啟用後,您登入時將須要使用此應用程式產生的認證碼。
manual_instructions: 如果您無法掃描 QR 圖形碼,請手動輸入︰
setup: 設定
warning: 如果您現在無法正確設定您的應用程式,請立刻「停用」雙因子認證,否則日後可能無法登入本站。
wrong_code: 您輸入的認證碼並不正確!可能伺服器時間和您手機不一致,請檢查您手機的時間,或與本站管理員聯絡。
users:
invalid_email: 信箱地址格式不正確
invalid_otp_token: 雙因子認證碼不正確
will_paginate:
page_gap: "&hellip;"

View file

@ -18,9 +18,9 @@ SimpleNavigation::Configuration.run do |navigation|
admin.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports}
admin.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts}
admin.item :pubsubhubbubs, safe_join([fa_icon('paper-plane-o fw'), t('admin.pubsubhubbub.title')]), admin_pubsubhubbub_index_url
admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_blocks.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}
admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }
admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }
admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_block.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}
admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url
admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url
admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), admin_settings_url
end

View file

@ -1,13 +1,17 @@
threads_count = ENV.fetch('MAX_THREADS') { 5 }.to_i
threads threads_count, threads_count
bind ENV.fetch('PORT') { 'unix:/home/mastodon/live/web.sock' }
port ENV.fetch('PORT') { 3000 }
environment ENV.fetch('RAILS_ENV') { 'development' }
workers ENV.fetch('WEB_CONCURRENCY') { 2 }
preload_app!
on_worker_boot do
if ENV['HEROKU'] # Spawn the workers from Puma, to only use one dyno
@sidekiq_pid ||= spawn('bundle exec sidekiq -q default -q push -q pull -q mailers ')
end
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end

View file

@ -1,5 +0,0 @@
class AddLanguageToStatuses < ActiveRecord::Migration[5.0]
def change
add_column :statuses, :language, :string, null: false, default: 'en'
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170414132105) do
ActiveRecord::Schema.define(version: 20170414080609) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -40,6 +40,7 @@ ActiveRecord::Schema.define(version: 20170414132105) do
t.datetime "header_updated_at"
t.string "avatar_remote_url"
t.datetime "subscription_expires_at"
t.datetime "last_webfingered_at"
t.boolean "silenced", default: false, null: false
t.boolean "suspended", default: false, null: false
t.boolean "locked", default: false, null: false
@ -47,7 +48,6 @@ ActiveRecord::Schema.define(version: 20170414132105) do
t.integer "statuses_count", default: 0, null: false
t.integer "followers_count", default: 0, null: false
t.integer "following_count", default: 0, null: false
t.datetime "last_webfingered_at"
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", using: :btree
t.index ["url"], name: "index_accounts_on_url", using: :btree
@ -244,7 +244,6 @@ ActiveRecord::Schema.define(version: 20170414132105) do
t.boolean "reply", default: false
t.integer "favourites_count", default: 0, null: false
t.integer "reblogs_count", default: 0, null: false
t.string "language", default: "en", null: false
t.index ["account_id"], name: "index_statuses_on_account_id", using: :btree
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", using: :btree
t.index ["reblog_of_id"], name: "index_statuses_on_reblog_of_id", using: :btree

View file

@ -1,16 +1,6 @@
# frozen_string_literal: true
namespace :mastodon do
desc 'Execute daily tasks'
task :daily do
Rake::Task['mastodon:feeds:clear'].invoke
Rake::Task['mastodon:media:clear'].invoke
Rake::Task['mastodon:users:clear'].invoke
Rake::Task['mastodon:push:refresh'].invoke
end
desc 'Turn a user into an admin, identified by the USERNAME environment variable'
task make_admin: :environment do
include RoutingHelper
@ -23,13 +13,12 @@ namespace :mastodon do
desc 'Manually confirms a user with associated user email address stored in USER_EMAIL environment variable.'
task confirm_email: :environment do
email = ENV.fetch('USER_EMAIL')
user = User.find_by(email: email)
user = User.where(email: email).first
if user
user.update(confirmed_at: Time.now.utc)
puts "#{email} confirmed"
puts "User #{email} confirmed."
else
abort "#{email} not found"
abort "User #{email} not found."
end
end
@ -43,13 +32,6 @@ namespace :mastodon do
task remove_silenced: :environment do
MediaAttachment.where(account: Account.silenced).find_each(&:destroy)
end
desc 'Remove cached remote media attachments that are older than a week'
task remove_remote: :environment do
MediaAttachment.where.not(remote_url: '').where('created_at < ?', 1.week.ago).find_each do |media|
media.file.destroy
end
end
end
namespace :push do
@ -78,7 +60,7 @@ namespace :mastodon do
end
end
desc 'Clears all timelines'
desc 'Clears all timelines so that they would be regenerated on next hit'
task clear_all: :environment do
Redis.current.keys('feed:*').each { |key| Redis.current.del(key) }
end
@ -94,14 +76,9 @@ namespace :mastodon do
end
namespace :users do
desc 'Clear out unconfirmed users'
desc 'clear unconfirmed users'
task clear: :environment do
# Users that never confirmed e-mail never signed in, means they
# only have a user record and an avatar record, with no files uploaded
User.where('confirmed_at is NULL AND confirmation_sent_at <= ?', 2.days.ago).find_in_batches do |batch|
Account.where(id: batch.map(&:account_id)).delete_all
User.where(id: batch.map(&:id)).delete_all
end
User.where('confirmed_at is NULL AND confirmation_sent_at <= ?', 2.days.ago).find_each(&:destroy)
end
end
@ -144,13 +121,8 @@ namespace :mastodon do
Rails.logger.debug 'Generating static avatars/headers for GIF ones...'
Account.unscoped.where(avatar_content_type: 'image/gif').or(Account.unscoped.where(header_content_type: 'image/gif')).find_each do |account|
begin
account.avatar.reprocess!
account.header.reprocess!
rescue StandardError => e
Rails.logger.error "Error while generating static avatars/headers for account #{account.id}: #{e}"
next
end
account.avatar.reprocess!
account.header.reprocess!
end
Rails.logger.debug 'Done!'

View file

@ -1,6 +1,5 @@
{
"name": "mastodon",
"license" : "AGPL-3.0",
"scripts": {
"start": "babel-node ./streaming/index.js --presets es2015,stage-2",
"storybook": "start-storybook -p 9001 -c storybook",

View file

@ -16,33 +16,4 @@ describe ApplicationHelper do
expect(result).to eq ""
end
end
describe 'show_landing_strip?', without_verify_partial_doubles: true do
describe 'when signed in' do
before do
allow(helper).to receive(:user_signed_in?).and_return(true)
end
it 'does not show landing strip' do
expect(helper.show_landing_strip?).to eq false
end
end
describe 'when signed out' do
before do
allow(helper).to receive(:user_signed_in?).and_return(false)
end
it 'does not show landing strip on single user instance' do
allow(helper).to receive(:single_user_mode?).and_return(true)
expect(helper.show_landing_strip?).to eq false
end
it 'shows landing strip on multi user instance' do
allow(helper).to receive(:single_user_mode?).and_return(false)
expect(helper.show_landing_strip?).to eq true
end
end
end
end

View file

@ -13,12 +13,6 @@ RSpec.configure do |config|
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
config.around(:example, :without_verify_partial_doubles) do |example|
mocks.verify_partial_doubles = false
example.call
mocks.verify_partial_doubles = true
end
end
config.before :each do

View file

@ -1,23 +1,67 @@
require 'rails_helper'
$LOAD_PATH << '../lib'
require 'tag_manager'
describe 'accounts/show.html.haml' do
describe 'stream_entries/show.html.haml' do
before do
allow(view).to receive(:show_landing_strip?).and_return(true)
double(api_oembed_url: '')
double(account_stream_entry_url: '')
def view.single_user_mode?
false
end
end
it 'has an h-feed with correct number of h-entry objects in it' do
alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
status = Fabricate(:status, account: alice, text: 'Hello World')
status2 = Fabricate(:status, account: alice, text: 'Hello World Again')
status3 = Fabricate(:status, account: alice, text: 'Are You Still There World?')
it 'has valid author h-card and basic data for a detailed_status' do
alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
bob = Fabricate(:account, username: 'bob', display_name: 'Bob')
status = Fabricate(:status, account: alice, text: 'Hello World')
Fabricate(:status, account: bob, thread: status, text: 'Hello Alice')
assign(:account, alice)
assign(:statuses, alice.statuses)
assign(:status, status)
assign(:stream_entry, status.stream_entry)
assign(:account, alice)
assign(:type, status.stream_entry.activity_type.downcase)
render
render(template: 'stream_entries/show.html.haml')
expect(Nokogiri::HTML(rendered).search('.h-feed .h-entry').size).to eq 3
mf2 = Microformats2.parse(rendered)
expect(mf2.entry.name.to_s).to eq status.text
expect(mf2.entry.url.to_s).not_to be_empty
expect(mf2.entry.author.format.name.to_s).to eq alice.display_name
expect(mf2.entry.author.format.url.to_s).not_to be_empty
end
it 'has valid h-cites for p-in-reply-to and p-comment' do
alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
bob = Fabricate(:account, username: 'bob', display_name: 'Bob')
carl = Fabricate(:account, username: 'carl', display_name: 'Carl')
status = Fabricate(:status, account: alice, text: 'Hello World')
reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice')
Fabricate(:status, account: carl, thread: reply, text: 'Hello Bob')
assign(:status, reply)
assign(:stream_entry, reply.stream_entry)
assign(:account, alice)
assign(:type, reply.stream_entry.activity_type.downcase)
assign(:ancestors, reply.stream_entry.activity.ancestors(bob))
assign(:descendants, reply.stream_entry.activity.descendants(bob))
render(template: 'stream_entries/show.html.haml')
mf2 = Microformats2.parse(rendered)
expect(mf2.entry.name.to_s).to eq reply.text
expect(mf2.entry.url.to_s).not_to be_empty
expect(mf2.entry.comment.format.url.to_s).not_to be_empty
expect(mf2.entry.comment.format.author.format.name.to_s).to eq carl.display_name
expect(mf2.entry.comment.format.author.format.url.to_s).not_to be_empty
expect(mf2.entry.in_reply_to.format.url.to_s).not_to be_empty
expect(mf2.entry.in_reply_to.format.author.format.name.to_s).to eq alice.display_name
expect(mf2.entry.in_reply_to.format.author.format.url.to_s).not_to be_empty
end
end

View file

@ -1,64 +1,23 @@
require 'rails_helper'
$LOAD_PATH << '../lib'
require 'tag_manager'
describe 'stream_entries/show.html.haml' do
describe 'accounts/show.html.haml' do
before do
double(:api_oembed_url => '')
double(:account_stream_entry_url => '')
allow(view).to receive(:show_landing_strip?).and_return(true)
def view.single_user_mode?
false
end
end
it 'has valid author h-card and basic data for a detailed_status' do
alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
bob = Fabricate(:account, username: 'bob', display_name: 'Bob')
status = Fabricate(:status, account: alice, text: 'Hello World')
reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice')
it 'has an h-feed with correct number of h-entry objects in it' do
alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
Fabricate(:status, account: alice, text: 'Hello World')
Fabricate(:status, account: alice, text: 'Hello World Again')
Fabricate(:status, account: alice, text: 'Are You Still There World?')
assign(:status, status)
assign(:stream_entry, status.stream_entry)
assign(:account, alice)
assign(:type, status.stream_entry.activity_type.downcase)
assign(:statuses, alice.statuses)
render
render(template: 'accounts/show.html.haml')
mf2 = Microformats2.parse(rendered)
expect(mf2.entry.name.to_s).to eq status.text
expect(mf2.entry.url.to_s).not_to be_empty
expect(mf2.entry.author.format.name.to_s).to eq alice.display_name
expect(mf2.entry.author.format.url.to_s).not_to be_empty
end
it 'has valid h-cites for p-in-reply-to and p-comment' do
alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
bob = Fabricate(:account, username: 'bob', display_name: 'Bob')
carl = Fabricate(:account, username: 'carl', display_name: 'Carl')
status = Fabricate(:status, account: alice, text: 'Hello World')
reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice')
comment = Fabricate(:status, account: carl, thread: reply, text: 'Hello Bob')
assign(:status, reply)
assign(:stream_entry, reply.stream_entry)
assign(:account, alice)
assign(:type, reply.stream_entry.activity_type.downcase)
assign(:ancestors, reply.stream_entry.activity.ancestors(bob) )
assign(:descendants, reply.stream_entry.activity.descendants(bob))
render
mf2 = Microformats2.parse(rendered)
expect(mf2.entry.name.to_s).to eq reply.text
expect(mf2.entry.url.to_s).not_to be_empty
expect(mf2.entry.comment.format.url.to_s).not_to be_empty
expect(mf2.entry.comment.format.author.format.name.to_s).to eq carl.display_name
expect(mf2.entry.comment.format.author.format.url.to_s).not_to be_empty
expect(mf2.entry.in_reply_to.format.url.to_s).not_to be_empty
expect(mf2.entry.in_reply_to.format.author.format.name.to_s).to eq alice.display_name
expect(mf2.entry.in_reply_to.format.author.format.url.to_s).not_to be_empty
expect(Nokogiri::HTML(rendered).search('.h-feed .h-entry').size).to eq 3
end
end