Merge branch 'master' into feature-language-detection
This commit is contained in:
commit
9f8375deaa
|
@ -6,3 +6,6 @@ node_modules
|
|||
storybook
|
||||
neo4j
|
||||
vendor/bundle
|
||||
.DS_Store
|
||||
*.swp
|
||||
*~
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Service dependencies
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
# REDIS_DB=0
|
||||
DB_HOST=db
|
||||
DB_USER=postgres
|
||||
DB_NAME=postgres
|
||||
|
@ -11,6 +12,10 @@ DB_PORT=5432
|
|||
LOCAL_DOMAIN=example.com
|
||||
LOCAL_HTTPS=true
|
||||
|
||||
# Use this only if you need to run mastodon on a different domain than the one used for federation.
|
||||
# Do not use this unless you know exactly what you are doing.
|
||||
# WEB_DOMAIN=mastodon.example.com
|
||||
|
||||
# Application secrets
|
||||
# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
|
||||
PAPERCLIP_SECRET=
|
||||
|
@ -41,6 +46,10 @@ SMTP_FROM_ADDRESS=notifications@example.com
|
|||
#SMTP_ENABLE_STARTTLS_AUTO=true
|
||||
|
||||
|
||||
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
|
||||
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
|
||||
# PAPERCLIP_ROOT_URL=/system
|
||||
|
||||
# Optional asset host for multi-server setups
|
||||
# CDN_HOST=assets.example.com
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# Federation
|
||||
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||
LOCAL_HTTPS=true
|
||||
OTP_SECRET=100c7faeef00caa29242f6b04156742bf76065771fd4117990c4282b8748ff3d99f8fdae97c982ab5bd2e6756a159121377cce4421f4a8ecd2d67bd7749a3fb4
|
||||
|
|
34
.eslintrc
34
.eslintrc
|
@ -8,7 +8,8 @@
|
|||
"parser": "babel-eslint",
|
||||
|
||||
"plugins": [
|
||||
"react"
|
||||
"react",
|
||||
"jsx-a11y"
|
||||
],
|
||||
|
||||
"parserOptions": {
|
||||
|
@ -43,9 +44,36 @@
|
|||
"no-mixed-spaces-and-tabs": 1,
|
||||
"no-nested-ternary": 1,
|
||||
"no-trailing-spaces": 1,
|
||||
"react/wrap-multilines": 2,
|
||||
|
||||
"react/jsx-wrap-multilines": 2,
|
||||
"react/self-closing-comp": 2,
|
||||
"react/prop-types": 2,
|
||||
"react/no-multi-comp": 0
|
||||
"react/no-multi-comp": 0,
|
||||
|
||||
"jsx-a11y/accessible-emoji": 1,
|
||||
"jsx-a11y/anchor-has-content": 1,
|
||||
"jsx-a11y/aria-activedescendant-has-tabindex": 1,
|
||||
"jsx-a11y/aria-props": 1,
|
||||
"jsx-a11y/aria-proptypes": 1,
|
||||
"jsx-a11y/aria-role": 1,
|
||||
"jsx-a11y/aria-unsupported-elements": 1,
|
||||
"jsx-a11y/heading-has-content": 1,
|
||||
"jsx-a11y/href-no-hash": 1,
|
||||
"jsx-a11y/html-has-lang": 1,
|
||||
"jsx-a11y/iframe-has-title": 1,
|
||||
"jsx-a11y/img-has-alt": 1,
|
||||
"jsx-a11y/img-redundant-alt": 1,
|
||||
"jsx-a11y/label-has-for": 1,
|
||||
"jsx-a11y/mouse-events-have-key-events": 1,
|
||||
"jsx-a11y/no-access-key": 1,
|
||||
"jsx-a11y/no-distracting-elements": 1,
|
||||
"jsx-a11y/no-onchange": 1,
|
||||
"jsx-a11y/no-redundant-roles": 1,
|
||||
"jsx-a11y/onclick-has-focus": 1,
|
||||
"jsx-a11y/onclick-has-role": 1,
|
||||
"jsx-a11y/role-has-required-aria-props": 1,
|
||||
"jsx-a11y/role-supports-aria-props": 1,
|
||||
"jsx-a11y/scope": 1,
|
||||
"jsx-a11y/tabindex-no-positive": 1
|
||||
}
|
||||
}
|
||||
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -29,10 +29,16 @@ neo4j/
|
|||
# Ignore Capistrano customizations
|
||||
config/deploy/*
|
||||
|
||||
|
||||
# Ignore IDE files
|
||||
.vscode/
|
||||
|
||||
# Ignore postgres + redis volume optionally created by docker-compose
|
||||
postgres
|
||||
redis
|
||||
|
||||
# Ignore Apple files
|
||||
.DS_Store
|
||||
|
||||
# Ignore vim files
|
||||
*~
|
||||
*.swp
|
||||
|
|
|
@ -5,8 +5,6 @@ notifications:
|
|||
email: false
|
||||
|
||||
env:
|
||||
matrix:
|
||||
- TRAVIS_NODE_VERSION="4"
|
||||
global:
|
||||
- LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||
- LOCAL_HTTPS=true
|
||||
|
@ -28,8 +26,7 @@ before_install:
|
|||
- sudo apt-get -qq update
|
||||
- sudo apt-get -qq install g++-4.8
|
||||
install:
|
||||
- nvm install $TRAVIS_NODE_VERSION
|
||||
- npm install -g npm@3
|
||||
- nvm install
|
||||
- npm install -g yarn
|
||||
- bundle install
|
||||
- yarn install
|
||||
|
|
|
@ -28,7 +28,7 @@ RUN BUILD_DEPS=" \
|
|||
imagemagick \
|
||||
&& npm install -g npm@3 && npm install -g yarn \
|
||||
&& bundle install --deployment --without test development \
|
||||
&& yarn \
|
||||
&& yarn --ignore-optional \
|
||||
&& yarn cache clean \
|
||||
&& npm -g cache clean \
|
||||
&& apk del $BUILD_DEPS \
|
||||
|
|
3
Gemfile
3
Gemfile
|
@ -44,6 +44,7 @@ gem 'rabl'
|
|||
gem 'rack-attack'
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
gem 'rack-timeout'
|
||||
gem 'rails-i18n'
|
||||
gem 'rails-settings-cached'
|
||||
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'rqrcode'
|
||||
|
@ -70,7 +71,9 @@ group :development, :test do
|
|||
end
|
||||
|
||||
group :test do
|
||||
gem 'capybara'
|
||||
gem 'faker'
|
||||
gem 'microformats2'
|
||||
gem 'rails-controller-testing'
|
||||
gem 'rspec-sidekiq'
|
||||
gem 'simplecov', require: false
|
||||
|
|
19
Gemfile.lock
19
Gemfile.lock
|
@ -99,6 +99,13 @@ GEM
|
|||
sshkit (~> 1.3)
|
||||
capistrano-yarn (2.0.2)
|
||||
capistrano (~> 3.0)
|
||||
capybara (2.13.0)
|
||||
addressable
|
||||
mime-types (>= 1.16)
|
||||
nokogiri (>= 1.3.3)
|
||||
rack (>= 1.0.0)
|
||||
rack-test (>= 0.5.4)
|
||||
xpath (~> 2.0)
|
||||
chunky_png (1.3.8)
|
||||
climate_control (0.1.0)
|
||||
cocaine (0.5.8)
|
||||
|
@ -233,6 +240,10 @@ GEM
|
|||
mail (2.6.4)
|
||||
mime-types (>= 1.16, < 4)
|
||||
method_source (0.8.2)
|
||||
microformats2 (2.1.0)
|
||||
activesupport
|
||||
json
|
||||
nokogiri
|
||||
mime-types (3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
|
@ -308,6 +319,9 @@ GEM
|
|||
nokogiri (~> 1.6)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
rails-i18n (5.0.3)
|
||||
i18n (~> 0.7)
|
||||
railties (~> 5.0)
|
||||
rails-settings-cached (0.6.5)
|
||||
rails (>= 4.2.0)
|
||||
rails_12factor (0.0.3)
|
||||
|
@ -447,6 +461,8 @@ GEM
|
|||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.2)
|
||||
whatlanguage (1.0.6)
|
||||
xpath (2.0.0)
|
||||
nokogiri (~> 1.3)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
@ -466,6 +482,7 @@ DEPENDENCIES
|
|||
capistrano-rails
|
||||
capistrano-rbenv
|
||||
capistrano-yarn
|
||||
capybara
|
||||
coffee-rails (~> 4.1.0)
|
||||
devise
|
||||
devise-two-factor
|
||||
|
@ -490,6 +507,7 @@ DEPENDENCIES
|
|||
letter_opener_web
|
||||
link_header
|
||||
lograge
|
||||
microformats2
|
||||
nokogiri
|
||||
oj
|
||||
ostatus2
|
||||
|
@ -507,6 +525,7 @@ DEPENDENCIES
|
|||
rack-timeout
|
||||
rails (~> 5.0.2)
|
||||
rails-controller-testing
|
||||
rails-i18n
|
||||
rails-settings-cached
|
||||
rails_12factor
|
||||
react-rails
|
||||
|
|
2
Vagrantfile
vendored
2
Vagrantfile
vendored
|
@ -43,7 +43,7 @@ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
|
|||
|
||||
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
||||
|
||||
export PATH="$HOME/.rbenv/bin::$PATH"
|
||||
export PATH="$HOME/.rbenv/bin:$PATH"
|
||||
eval "$(rbenv init -)"
|
||||
|
||||
cd /vagrant
|
||||
|
|
82
app/assets/javascripts/components/actions/mutes.jsx
Normal file
82
app/assets/javascripts/components/actions/mutes.jsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
import api, { getLinks } from '../api'
|
||||
import { fetchRelationships } from './accounts';
|
||||
|
||||
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
|
||||
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
|
||||
export const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL';
|
||||
|
||||
export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
|
||||
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
|
||||
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
|
||||
|
||||
export function fetchMutes() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchMutesRequest());
|
||||
|
||||
api(getState).get('/api/v1/mutes').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => dispatch(fetchMutesFail(error)));
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMutesRequest() {
|
||||
return {
|
||||
type: MUTES_FETCH_REQUEST
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMutesSuccess(accounts, next) {
|
||||
return {
|
||||
type: MUTES_FETCH_SUCCESS,
|
||||
accounts,
|
||||
next
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMutesFail(error) {
|
||||
return {
|
||||
type: MUTES_FETCH_FAIL,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMutes() {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['user_lists', 'mutes', 'next']);
|
||||
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandMutesRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => dispatch(expandMutesFail(error)));
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMutesRequest() {
|
||||
return {
|
||||
type: MUTES_EXPAND_REQUEST
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMutesSuccess(accounts, next) {
|
||||
return {
|
||||
type: MUTES_EXPAND_SUCCESS,
|
||||
accounts,
|
||||
next
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMutesFail(error) {
|
||||
return {
|
||||
type: MUTES_EXPAND_FAIL,
|
||||
error
|
||||
};
|
||||
};
|
|
@ -10,7 +10,8 @@ const messages = defineMessages({
|
|||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' }
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute' }
|
||||
});
|
||||
|
||||
const buttonsStyle = {
|
||||
|
@ -25,6 +26,7 @@ const Account = React.createClass({
|
|||
me: React.PropTypes.number.isRequired,
|
||||
onFollow: React.PropTypes.func.isRequired,
|
||||
onBlock: React.PropTypes.func.isRequired,
|
||||
onMute: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
|
@ -38,6 +40,10 @@ const Account = React.createClass({
|
|||
this.props.onBlock(this.props.account);
|
||||
},
|
||||
|
||||
handleMute () {
|
||||
this.props.onMute(this.props.account);
|
||||
},
|
||||
|
||||
render () {
|
||||
const { account, me, intl } = this.props;
|
||||
|
||||
|
@ -51,11 +57,14 @@ const Account = React.createClass({
|
|||
const following = account.getIn(['relationship', 'following']);
|
||||
const requested = account.getIn(['relationship', 'requested']);
|
||||
const blocking = account.getIn(['relationship', 'blocking']);
|
||||
const muting = account.getIn(['relationship', 'muting']);
|
||||
|
||||
if (requested) {
|
||||
buttons = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />
|
||||
} else if (blocking) {
|
||||
buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
|
||||
buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||
} else if (muting) {
|
||||
buttons = <IconButton active={true} icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
|
||||
} else {
|
||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||
}
|
||||
|
|
|
@ -178,7 +178,12 @@ const AutosuggestTextarea = React.createClass({
|
|||
|
||||
<div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'>
|
||||
{suggestions.map((suggestion, i) => (
|
||||
<div key={suggestion} className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} onClick={this.onSuggestionClick.bind(this, suggestion)}>
|
||||
<div
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
key={suggestion}
|
||||
className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`}
|
||||
onClick={this.onSuggestionClick.bind(this, suggestion)}>
|
||||
<AutosuggestAccountContainer id={suggestion} />
|
||||
</div>
|
||||
))}
|
||||
|
|
|
@ -9,6 +9,7 @@ const Button = React.createClass({
|
|||
block: React.PropTypes.bool,
|
||||
secondary: React.PropTypes.bool,
|
||||
size: React.PropTypes.number,
|
||||
style: React.PropTypes.object,
|
||||
children: React.PropTypes.node
|
||||
},
|
||||
|
||||
|
|
|
@ -15,13 +15,13 @@ const ColumnBackButton = React.createClass({
|
|||
mixins: [PureRenderMixin],
|
||||
|
||||
handleClick () {
|
||||
if (window.history && window.history.length == 1) this.context.router.push("/");
|
||||
if (window.history && window.history.length === 1) this.context.router.push("/");
|
||||
else this.context.router.goBack();
|
||||
},
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div onClick={this.handleClick} className='column-back-button'>
|
||||
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'>
|
||||
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</div>
|
||||
|
|
|
@ -31,7 +31,7 @@ const ColumnBackButtonSlim = React.createClass({
|
|||
render () {
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={outerStyle} onClick={this.handleClick} className='column-back-button'>
|
||||
<div role='button' tabIndex='0' style={outerStyle} onClick={this.handleClick} className='column-back-button'>
|
||||
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</div>
|
||||
|
|
|
@ -46,7 +46,9 @@ const ColumnCollapsable = React.createClass({
|
|||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
|
||||
<div role='button' tabIndex='0' title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}>
|
||||
<i className={`fa fa-${icon}`} />
|
||||
</div>
|
||||
|
||||
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
|
||||
{({ opacity, height }) =>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const LoadMore = ({ onClick }) => (
|
||||
<a href='#' className='load-more' onClick={onClick}>
|
||||
<a href="#" className='load-more' role='button' onClick={onClick}>
|
||||
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
||||
</a>
|
||||
);
|
||||
|
|
|
@ -220,7 +220,7 @@ const MediaGallery = React.createClass({
|
|||
}
|
||||
|
||||
children = (
|
||||
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
||||
<div role='button' tabIndex='0' style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
||||
<span style={spoilerSpanStyle}>{warning}</span>
|
||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,8 @@ const Permalink = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
href: React.PropTypes.string.isRequired,
|
||||
to: React.PropTypes.string.isRequired
|
||||
to: React.PropTypes.string.isRequired,
|
||||
children: React.PropTypes.node
|
||||
},
|
||||
|
||||
handleClick (e) {
|
||||
|
|
|
@ -92,10 +92,14 @@ const StatusActionBar = React.createClass({
|
|||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||
}
|
||||
|
||||
let reblogIcon = 'retweet';
|
||||
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
|
||||
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
||||
<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={status.get('visibility') === 'direct' ? 'envelope' : (status.get('visibility') === 'private' ? 'lock' : 'retweet')} onClick={this.handleReblogClick} /></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>
|
||||
|
||||
<div style={{ width: '18px', height: '18px', float: 'left' }}>
|
||||
|
|
|
@ -119,7 +119,7 @@ const StatusContent = React.createClass({
|
|||
return (
|
||||
<div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||
<p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} >
|
||||
<span dangerouslySetInnerHTML={spoilerContent} /> <a className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>{toggleText}</a>
|
||||
<span dangerouslySetInnerHTML={spoilerContent} /> <a tabIndex='0' className='status__content__spoiler-link' role='button' onClick={this.handleSpoilerClick}>{toggleText}</a>
|
||||
</p>
|
||||
|
||||
{mentionsPlaceholder}
|
||||
|
|
|
@ -194,7 +194,7 @@ const VideoPlayer = React.createClass({
|
|||
if (!this.state.visible) {
|
||||
if (sensitive) {
|
||||
return (
|
||||
<div style={{...spoilerStyle, 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>
|
||||
|
@ -202,7 +202,7 @@ const VideoPlayer = React.createClass({
|
|||
);
|
||||
} else {
|
||||
return (
|
||||
<div style={{...spoilerStyle, 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>
|
||||
|
@ -213,7 +213,7 @@ const VideoPlayer = React.createClass({
|
|||
|
||||
if (this.state.preview && !autoplay) {
|
||||
return (
|
||||
<div style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
|
||||
<div role='button' tabIndex='0' style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
|
||||
{spoilerButton}
|
||||
<div style={{ position: 'absolute', top: '50%', left: '50%', fontSize: '36px', transform: 'translate(-50%, -50%)', padding: '5px', borderRadius: '100px', color: 'rgba(255, 255, 255, 0.8)' }}><i className='fa fa-play' /></div>
|
||||
</div>
|
||||
|
@ -225,7 +225,7 @@ const VideoPlayer = React.createClass({
|
|||
{spoilerButton}
|
||||
{muteButton}
|
||||
{expandButton}
|
||||
<video ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
|
||||
<video role='button' tabIndex='0' ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import FollowRequests from '../features/follow_requests';
|
|||
import GenericNotFound from '../features/generic_not_found';
|
||||
import FavouritedStatuses from '../features/favourited_statuses';
|
||||
import Blocks from '../features/blocks';
|
||||
import Mutes from '../features/mutes';
|
||||
import Report from '../features/report';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import en from 'react-intl/locale-data/en';
|
||||
|
@ -60,8 +61,8 @@ import { hydrateStore } from '../actions/store';
|
|||
import createStream from '../stream';
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
store.dispatch(hydrateStore(window.INITIAL_STATE));
|
||||
const initialState = JSON.parse(document.getElementById("initial-state").textContent);
|
||||
store.dispatch(hydrateStore(initialState));
|
||||
|
||||
const browserHistory = useRouterHistory(createBrowserHistory)({
|
||||
basename: '/web'
|
||||
|
@ -94,9 +95,10 @@ const Mastodon = React.createClass({
|
|||
|
||||
componentDidMount() {
|
||||
const { locale } = this.props;
|
||||
const streamingAPIBaseURL = store.getState().getIn(['meta', 'streaming_api_base_url']);
|
||||
const accessToken = store.getState().getIn(['meta', 'access_token']);
|
||||
|
||||
this.subscription = createStream(accessToken, 'user', {
|
||||
this.subscription = createStream(streamingAPIBaseURL, accessToken, 'user', {
|
||||
|
||||
connected () {
|
||||
store.dispatch(connectTimeline('home'));
|
||||
|
@ -171,6 +173,7 @@ const Mastodon = React.createClass({
|
|||
|
||||
<Route path='follow_requests' component={FollowRequests} />
|
||||
<Route path='blocks' component={Blocks} />
|
||||
<Route path='mutes' component={Mutes} />
|
||||
<Route path='report' component={Report} />
|
||||
|
||||
<Route path='*' component={GenericNotFound} />
|
||||
|
|
|
@ -43,7 +43,16 @@ const Avatar = React.createClass({
|
|||
return (
|
||||
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
|
||||
{({ radius }) =>
|
||||
<a href={account.get('url')} className='account__header__avatar' target='_blank' rel='noopener' style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }} onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
|
||||
<a
|
||||
href={account.get('url')}
|
||||
className='account__header__avatar'
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }}
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseOut={this.handleMouseOut}
|
||||
onFocus={this.handleMouseOver}
|
||||
onBlur={this.handleMouseOut}>
|
||||
<img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
|
||||
</a>
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ const messages = defineMessages({
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
|
||||
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||
accessToken: state.getIn(['meta', 'access_token'])
|
||||
});
|
||||
|
||||
|
@ -29,6 +30,7 @@ const CommunityTimeline = React.createClass({
|
|||
propTypes: {
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||
accessToken: React.PropTypes.string.isRequired,
|
||||
hasUnread: React.PropTypes.bool
|
||||
},
|
||||
|
@ -36,7 +38,7 @@ const CommunityTimeline = React.createClass({
|
|||
mixins: [PureRenderMixin],
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, accessToken } = this.props;
|
||||
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
|
||||
|
||||
dispatch(refreshTimeline('community'));
|
||||
|
||||
|
@ -44,7 +46,7 @@ const CommunityTimeline = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
subscription = createStream(accessToken, 'public:local', {
|
||||
subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
|
||||
|
||||
connected () {
|
||||
dispatch(connectTimeline('community'));
|
||||
|
|
|
@ -83,7 +83,7 @@ const PrivacyDropdown = React.createClass({
|
|||
<div className='privacy-dropdown__value'><IconButton icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
|
||||
<div className='privacy-dropdown__dropdown'>
|
||||
{options.map(item =>
|
||||
<div key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
|
||||
<div role='button' tabIndex='0' key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
|
||||
<div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{item.shortText}</strong>
|
||||
|
|
|
@ -36,6 +36,10 @@ const Search = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
noop () {
|
||||
|
||||
},
|
||||
|
||||
handleFocus () {
|
||||
this.props.onShow();
|
||||
},
|
||||
|
@ -56,9 +60,9 @@ const Search = React.createClass({
|
|||
onFocus={this.handleFocus}
|
||||
/>
|
||||
|
||||
<div className='search__icon'>
|
||||
<div role='button' tabIndex='0' className='search__icon' onClick={hasValue ? this.handleClear : this.noop}>
|
||||
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
|
||||
<i className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} onClick={this.handleClear} />
|
||||
<i aria-label="Clear search" className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -50,7 +50,7 @@ const Followers = React.createClass({
|
|||
|
||||
handleLoadMore (e) {
|
||||
e.preventDefault();
|
||||
this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
|
||||
this.props.dispatch(expandFollowers(Number(this.props.params.accountId)));
|
||||
},
|
||||
|
||||
render () {
|
||||
|
|
|
@ -14,6 +14,7 @@ const messages = defineMessages({
|
|||
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }
|
||||
});
|
||||
|
||||
|
@ -37,6 +38,7 @@ const GettingStarted = ({ intl, me }) => {
|
|||
<ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
|
||||
{followRequests}
|
||||
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
||||
<ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
|
||||
<ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
|
||||
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
|
||||
</div>
|
||||
|
|
|
@ -13,6 +13,7 @@ import createStream from '../../stream';
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
|
||||
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||
accessToken: state.getIn(['meta', 'access_token'])
|
||||
});
|
||||
|
||||
|
@ -21,6 +22,7 @@ const HashtagTimeline = React.createClass({
|
|||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||
accessToken: React.PropTypes.string.isRequired,
|
||||
hasUnread: React.PropTypes.bool
|
||||
},
|
||||
|
@ -28,9 +30,9 @@ const HashtagTimeline = React.createClass({
|
|||
mixins: [PureRenderMixin],
|
||||
|
||||
_subscribe (dispatch, id) {
|
||||
const { accessToken } = this.props;
|
||||
const { streamingAPIBaseURL, accessToken } = this.props;
|
||||
|
||||
this.subscription = createStream(accessToken, `hashtag&tag=${id}`, {
|
||||
this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, {
|
||||
|
||||
received (data) {
|
||||
switch(data.event) {
|
||||
|
|
68
app/assets/javascripts/components/features/mutes/index.jsx
Normal file
68
app/assets/javascripts/components/features/mutes/index.jsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import { ScrollContainer } from 'react-router-scroll';
|
||||
import Column from '../ui/components/column';
|
||||
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import { fetchMutes, expandMutes } from '../../actions/mutes';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.mutes', defaultMessage: 'Muted users' }
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'mutes', 'items'])
|
||||
});
|
||||
|
||||
const Mutes = React.createClass({
|
||||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchMutes());
|
||||
},
|
||||
|
||||
handleScroll (e) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
|
||||
if (scrollTop === scrollHeight - clientHeight) {
|
||||
this.props.dispatch(expandMutes());
|
||||
}
|
||||
},
|
||||
|
||||
render () {
|
||||
const { intl, accountIds } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column icon='users' heading={intl.formatMessage(messages.heading)}>
|
||||
<ColumnBackButtonSlim />
|
||||
<ScrollContainer scrollKey='mutes'>
|
||||
<div className='scrollable' onScroll={this.handleScroll}>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} />
|
||||
)}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Mutes));
|
|
@ -15,7 +15,7 @@ const ClearColumnButton = React.createClass({
|
|||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<div title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
|
||||
<div role='button' title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
|
||||
<i className='fa fa-eraser' />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -27,9 +27,11 @@ const ColumnSettings = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
onSave: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.shape({
|
||||
formatMessage: React.PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
|
|
@ -71,7 +71,7 @@ const Notification = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
render () {
|
||||
render () { // eslint-disable-line consistent-return
|
||||
const { notification } = this.props;
|
||||
const account = notification.get('account');
|
||||
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
|
||||
|
|
|
@ -14,8 +14,8 @@ const labelSpanStyle = {
|
|||
marginLeft: '8px'
|
||||
};
|
||||
|
||||
const SettingToggle = ({ settings, settingKey, label, onChange }) => (
|
||||
<label style={labelStyle}>
|
||||
const SettingToggle = ({ settings, settingKey, label, onChange, htmlFor = '' }) => (
|
||||
<label htmlFor={htmlFor} style={labelStyle}>
|
||||
<Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
|
||||
<span className='setting-toggle' style={labelSpanStyle}>{label}</span>
|
||||
</label>
|
||||
|
@ -25,7 +25,8 @@ SettingToggle.propTypes = {
|
|||
settings: ImmutablePropTypes.map.isRequired,
|
||||
settingKey: React.PropTypes.array.isRequired,
|
||||
label: React.PropTypes.node.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
htmlFor: React.PropTypes.string
|
||||
};
|
||||
|
||||
export default SettingToggle;
|
||||
|
|
|
@ -19,6 +19,7 @@ const messages = defineMessages({
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
|
||||
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||
accessToken: state.getIn(['meta', 'access_token'])
|
||||
});
|
||||
|
||||
|
@ -29,6 +30,7 @@ const PublicTimeline = React.createClass({
|
|||
propTypes: {
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||
accessToken: React.PropTypes.string.isRequired,
|
||||
hasUnread: React.PropTypes.bool
|
||||
},
|
||||
|
@ -36,7 +38,7 @@ const PublicTimeline = React.createClass({
|
|||
mixins: [PureRenderMixin],
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, accessToken } = this.props;
|
||||
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
|
||||
|
||||
dispatch(refreshTimeline('public'));
|
||||
|
||||
|
@ -44,7 +46,7 @@ const PublicTimeline = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
subscription = createStream(accessToken, 'public', {
|
||||
subscription = createStream(streamingAPIBaseURL, accessToken, 'public', {
|
||||
|
||||
connected () {
|
||||
dispatch(connectTimeline('public'));
|
||||
|
|
|
@ -71,10 +71,14 @@ const ActionBar = React.createClass({
|
|||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||
}
|
||||
|
||||
let reblogIcon = 'retweet';
|
||||
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
|
||||
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
|
||||
|
||||
return (
|
||||
<div className='detailed-status__action-bar'>
|
||||
<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={status.get('visibility') === 'direct' ? 'envelope' : (status.get('visibility') === 'private' ? 'lock' : 'retweet')} onClick={this.handleReblogClick} /></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>
|
||||
</div>
|
||||
|
|
|
@ -25,7 +25,7 @@ const ColumnHeader = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div aria-label={type} className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick}>
|
||||
<div role='button' tabIndex='0' aria-label={type} className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick}>
|
||||
{icon}
|
||||
{type}
|
||||
</div>
|
||||
|
|
|
@ -34,7 +34,8 @@ ColumnLink.propTypes = {
|
|||
icon: React.PropTypes.string.isRequired,
|
||||
text: React.PropTypes.string.isRequired,
|
||||
to: React.PropTypes.string,
|
||||
href: React.PropTypes.string
|
||||
href: React.PropTypes.string,
|
||||
method: React.PropTypes.string
|
||||
};
|
||||
|
||||
export default ColumnLink;
|
||||
|
|
|
@ -104,8 +104,8 @@ const MediaModal = React.createClass({
|
|||
leftNav = rightNav = content = '';
|
||||
|
||||
if (media.size > 1) {
|
||||
leftNav = <div style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
|
||||
rightNav = <div style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
|
||||
leftNav = <div role='button' tabIndex='0' style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
|
||||
rightNav = <div role='button' tabIndex='0' style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
|
||||
}
|
||||
|
||||
if (attachment.get('type') === 'image') {
|
||||
|
|
|
@ -66,7 +66,7 @@ const ModalRoot = React.createClass({
|
|||
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} />
|
||||
<div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} />
|
||||
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
|
||||
<SpecificComponent {...props} onClose={onClose} />
|
||||
</div>
|
||||
|
|
|
@ -141,7 +141,7 @@ const UI = React.createClass({
|
|||
{mountedColumns}
|
||||
|
||||
<NotificationsContainer />
|
||||
<LoadingBarContainer style={{ backgroundColor: '#2b90d9', left: '0', top: '0' }} />
|
||||
<LoadingBarContainer className="loading-bar" />
|
||||
<ModalContainer />
|
||||
<UploadArea active={draggingOver} />
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ Link.parseAttrs = (link, parts) => {
|
|||
link = Link.parseParams(link, uriAttrs[1])
|
||||
}
|
||||
|
||||
while(match = Link.attrPattern.exec(attrs)) {
|
||||
while(match = Link.attrPattern.exec(attrs)) { // eslint-disable-line no-cond-assign
|
||||
attr = match[1].toLowerCase()
|
||||
value = match[4] || match[3] || match[2]
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ const en = {
|
|||
"column.favourites": "Favourites",
|
||||
"column.follow_requests": "Follow requests",
|
||||
"column.home": "Home",
|
||||
"column.mutes": "Muted users",
|
||||
"column.notifications": "Notifications",
|
||||
"column.public": "Federated timeline",
|
||||
"compose_form.placeholder": "What is on your mind?",
|
||||
|
@ -68,6 +69,7 @@ const en = {
|
|||
"navigation_bar.follow_requests": "Follow requests",
|
||||
"navigation_bar.info": "Extended information",
|
||||
"navigation_bar.logout": "Logout",
|
||||
"navigation_bar.mutes": "Muted users",
|
||||
"navigation_bar.preferences": "Preferences",
|
||||
"navigation_bar.public_timeline": "Federated timeline",
|
||||
"notification.favourite": "{name} favourited your status",
|
||||
|
|
|
@ -37,6 +37,7 @@ const ja = {
|
|||
"getting_started.about_addressing": "ドメインとユーザー名を知っているなら検索フォームに入力すればフォローできます。",
|
||||
"getting_started.about_shortcuts": "対象のアカウントがあなたと同じドメインのユーザーならばユーザー名のみで検索できます。これは返信のときも一緒です。",
|
||||
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub({github})から開発に参加したり、問題を報告したりできます。 {apps}",
|
||||
"getting_started.apps": "さまざまなアプリで利用できます。",
|
||||
"column.home": "ホーム",
|
||||
"column.community": "ローカルタイムライン",
|
||||
"column.public": "連合タイムライン",
|
||||
|
@ -64,7 +65,7 @@ const ja = {
|
|||
"privacy.private.long": "フォロワーだけに公開",
|
||||
"privacy.direct.short": "ダイレクト",
|
||||
"privacy.direct.long": "含んだユーザーだけに公開",
|
||||
"privacy.change": "投稿のプライバシーを変更2",
|
||||
"privacy.change": "投稿のプライバシーを変更",
|
||||
"report.heading": "新規通報",
|
||||
"report.placeholder": "コメント",
|
||||
"report.target": "問題のユーザー",
|
||||
|
@ -82,6 +83,7 @@ const ja = {
|
|||
"search.account": "アカウント",
|
||||
"search.hashtag": "ハッシュタグ",
|
||||
"search.status_by": "{uuuname}からの投稿",
|
||||
"search_results.total": "{count} 件",
|
||||
"upload_area.title": "ファイルをこちらにドラッグしてください",
|
||||
"upload_button.label": "メディアを追加",
|
||||
"upload_form.undo": "やり直す",
|
||||
|
@ -111,7 +113,7 @@ const ja = {
|
|||
"home.column_settings.show_replies": "返信表示",
|
||||
"home.column_settings.filter_regex": "正規表現でフィルター",
|
||||
"home.settings": "カラム設定",
|
||||
"notification.settings": "カラム設定",
|
||||
"notifications.settings": "カラム設定",
|
||||
"missing_indicator.label": "見つかりません",
|
||||
"boost_modal.combo": "次は{combo}を押せば、これをスキップできます。"
|
||||
};
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
const nl = {
|
||||
"column_back_button.label": "terug",
|
||||
"lightbox.close": "Sluiten",
|
||||
"loading_indicator.label": "Laden...",
|
||||
"status.mention": "Vermeld @{name}",
|
||||
"status.delete": "Verwijder",
|
||||
"status.reply": "Reageer",
|
||||
"loading_indicator.label": "Laden…",
|
||||
"status.mention": "@{name} vermelden",
|
||||
"status.delete": "Verwijderen",
|
||||
"status.reply": "Reageren",
|
||||
"status.reblog": "Boost",
|
||||
"status.favourite": "Favoriet",
|
||||
"status.reblogged_by": "{name} boostte",
|
||||
"status.sensitive_warning": "Gevoelige inhoud",
|
||||
"status.sensitive_toggle": "Klik om te zien",
|
||||
"video_player.toggle_sound": "Geluid omschakelen",
|
||||
"account.mention": "Vermeld @{name}",
|
||||
"account.edit_profile": "Bewerk profiel",
|
||||
"account.unblock": "Deblokkeer @{name}",
|
||||
"account.unfollow": "Ontvolg",
|
||||
"account.block": "Blokkeer @{name}",
|
||||
"account.follow": "Volg",
|
||||
"video_player.toggle_sound": "Geluid in-/uitschakelen",
|
||||
"account.mention": "@{name} vermelden",
|
||||
"account.edit_profile": "Profiel bewerken",
|
||||
"account.unblock": "@{name} deblokkeren",
|
||||
"account.unfollow": "Ontvolgen",
|
||||
"account.block": "@{name} blokkeren",
|
||||
"account.follow": "Volgen",
|
||||
"account.posts": "Berichten",
|
||||
"account.follows": "Volgt",
|
||||
"account.followers": "Volgers",
|
||||
|
@ -25,7 +25,7 @@ const nl = {
|
|||
"getting_started.heading": "Beginnen",
|
||||
"getting_started.about_addressing": "Je kunt mensen volgen als je hun gebruikersnaam en het domein van hun server kent, door het e-mailachtige adres in het zoekscherm in te voeren.",
|
||||
"getting_started.about_shortcuts": "Als de gezochte gebruiker op hetzelfde domein zit als jijzelf, is invoeren van de gebruikersnaam genoeg. Dat geldt ook als je mensen in de statussen wilt vermelden.",
|
||||
"getting_started.open_source_notice": "Mastodon is open source software. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.",
|
||||
"getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.",
|
||||
"column.home": "Thuis",
|
||||
"column.community": "Lokale tijdlijn",
|
||||
"column.public": "Federatietijdlijn",
|
||||
|
@ -37,30 +37,30 @@ const nl = {
|
|||
"tabs_bar.notifications": "Meldingen",
|
||||
"compose_form.placeholder": "Waar ben je mee bezig?",
|
||||
"compose_form.publish": "Toot",
|
||||
"compose_form.sensitive": "Markeer media als gevoelig",
|
||||
"compose_form.spoiler": "Verberg tekst achter waarschuwing",
|
||||
"compose_form.private": "Mark als privé",
|
||||
"compose_form.privacy_disclaimer": "Je besloten status wordt afgeleverd aan vermelde gebruikers op {domains}. Vertrouw je {domainsCount, plural, one {that server} andere {those servers}}? Privé plaatsen werkt alleen op Mastodon servers. Als {domains} {domainsCount, plural, een {is not a Mastodon instance} andere {are not Mastodon instances}}, dan wordt er geen indicatie gegeven dat he bericht besloten is, waardoor het kan worden geboost of op andere manier zichtbaar worden voor niet bedoelde lezers.",
|
||||
"compose_form.unlisted": "Niet tonen op openbare tijdlijnen",
|
||||
"navigation_bar.edit_profile": "Bewerk profiel",
|
||||
"compose_form.sensitive": "Media als gevoelig markeren",
|
||||
"compose_form.spoiler": "Tekst achter waarschuwing verbergen",
|
||||
"compose_form.private": "Als privé markeren",
|
||||
"compose_form.privacy_disclaimer": "Je besloten status wordt afgeleverd aan vermelde gebruikers op {domains}. Vertrouw je {domainsCount, plural, one {that server} andere {those servers}}? Privé plaatsen werkt alleen op Mastodon servers. Als {domains} {domainsCount, plural, een {is not a Mastodon instance} andere {are not Mastodon instances}}, dan wordt er geen indicatie gegeven dat he bericht besloten is, waardoor het kan worden geboost of op andere manier zichtbaar worden voor niet bedoelde lezers.",
|
||||
"compose_form.unlisted": "Niet op openbare tijdlijnen tonen",
|
||||
"navigation_bar.edit_profile": "Profiel bewerken",
|
||||
"navigation_bar.preferences": "Voorkeuren",
|
||||
"navigation_bar.community_timeline": "Lokale tijdlijn",
|
||||
"navigation_bar.public_timeline": "Federatietijdlijn",
|
||||
"navigation_bar.logout": "Uitloggen",
|
||||
"navigation_bar.logout": "Afmelden",
|
||||
"reply_indicator.cancel": "Annuleren",
|
||||
"search.placeholder": "Zoeken",
|
||||
"search.account": "Account",
|
||||
"search.hashtag": "Hashtag",
|
||||
"upload_button.label": "Toevoegen media",
|
||||
"upload_button.label": "Media toevoegen",
|
||||
"upload_form.undo": "Ongedaan maken",
|
||||
"notification.follow": "{name} volgde jou",
|
||||
"notification.favourite": "{name} markeerde je status als favoriet",
|
||||
"notification.reblog": "{name} boostte je status",
|
||||
"notification.mention": "{name} vermeldde jou",
|
||||
"notifications.column_settings.alert": "Desktopmeldingen",
|
||||
"notifications.column_settings.show": "Tonen in kolom",
|
||||
"notifications.column_settings.show": "In kolom tonen",
|
||||
"notifications.column_settings.follow": "Nieuwe volgers:",
|
||||
"notifications.column_settings.favourite": "Favoriten:",
|
||||
"notifications.column_settings.favourite": "Favorieten:",
|
||||
"notifications.column_settings.mention": "Vermeldingen:",
|
||||
"notifications.column_settings.reblog": "Boosts:",
|
||||
};
|
||||
|
|
|
@ -1,77 +1,130 @@
|
|||
const no = {
|
||||
"column_back_button.label": "Tilbake",
|
||||
"lightbox.close": "Lukk",
|
||||
"loading_indicator.label": "Laster...",
|
||||
"status.mention": "Nevn @{name}",
|
||||
"status.delete": "Slett",
|
||||
"status.reply": "Svar",
|
||||
"status.reblog": "Reblogg",
|
||||
"status.favourite": "Lik",
|
||||
"status.reblogged_by": "{name} reblogget",
|
||||
"status.sensitive_warning": "Sensitivt innhold",
|
||||
"status.sensitive_toggle": "Klikk for å vise",
|
||||
"status.show_more": "Vis mer",
|
||||
"status.show_less": "Vis mindre",
|
||||
"status.open": "Utvid denne statusen",
|
||||
"status.report": "Rapporter @{name}",
|
||||
"video_player.toggle_sound": "Veksle lyd",
|
||||
"account.mention": "Nevn @{name}",
|
||||
"account.block": "Blokkér @{name}",
|
||||
"account.disclaimer": "Denne brukeren er fra en annen instans. Dette tallet kan være høyere.",
|
||||
"account.edit_profile": "Rediger profil",
|
||||
"account.follow": "Følg",
|
||||
"account.followers": "Følgere",
|
||||
"account.follows_you": "Følger deg",
|
||||
"account.follows": "Følginger",
|
||||
"account.mention": "Nevn @{name}",
|
||||
"account.mute": "Demp @{name}",
|
||||
"account.posts": "Poster",
|
||||
"account.report": "Rapportér @{name}",
|
||||
"account.requested": "Venter på godkjennelse",
|
||||
"account.unblock": "Avblokker @{name}",
|
||||
"account.unfollow": "Avfølg",
|
||||
"account.block": "Blokker @{name}",
|
||||
"account.follow": "Følg",
|
||||
"account.posts": "Poster",
|
||||
"account.follows": "Følginger",
|
||||
"account.followers": "Følgere",
|
||||
"account.follows_you": "Folger deg",
|
||||
"account.requested": "Venter på godkjennelse",
|
||||
"getting_started.heading": "Kom i gang",
|
||||
"getting_started.about_addressing": "Du kan følge noen hvis du vet brukernavnet deres og domenet de er på ved å skrive en e-postadresse inn i søkeskjemaet.",
|
||||
"getting_started.about_shortcuts": "Hvis målbrukeren er på samme domene som deg, vil kun brukernavnet også fungere. Den samme regelen gjelder når man nevner noen i statuser.",
|
||||
"getting_started.open_source_notice": "Mastodon er programvare med fri kildekode. Du kan bidra eller rapportere problemer på GitHub på {github}. {apps}.",
|
||||
"column.home": "Hjem",
|
||||
"column.community": "Lokal tidslinje",
|
||||
"column.public": "Forent tidslinje",
|
||||
"column.notifications": "Varslinger",
|
||||
"account.unmute": "Avdemp @{name}",
|
||||
"boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
|
||||
"column_back_button.label": "Tilbake",
|
||||
"column.blocks": "Blokkerte brukere",
|
||||
"column.community": "Lokal tidslinje",
|
||||
"column.favourites": "Likt",
|
||||
"tabs_bar.compose": "Komponer",
|
||||
"tabs_bar.home": "Hjem",
|
||||
"tabs_bar.mentions": "Nevninger",
|
||||
"tabs_bar.public": "Forent tidslinje",
|
||||
"tabs_bar.notifications": "Varslinger",
|
||||
"column.follow_requests": "Følgeforespørsler",
|
||||
"column.home": "Hjem",
|
||||
"column.notifications": "Varslinger",
|
||||
"column.public": "Felles tidslinje",
|
||||
"compose_form.placeholder": "Hva har du på hjertet?",
|
||||
"compose_form.privacy_disclaimer": "Din private status vil leveres til nevnte brukere på {domains}. Stoler du på {domainsCount, plural, one {den serveren} other {de serverne}}? Synlighet fungerer kun på Mastodon-instanser. Hvis {domains} {domainsCount, plural, one {ike er en Mastodon-instans} other {ikke er Mastodon-instanser}}, vil det ikke indikeres at posten din er privat, og den kan kanskje bli fremhevd eller på annen måte bli synlig for uventede mottakere.",
|
||||
"compose_form.publish": "Tut",
|
||||
"compose_form.sensitive": "Merk media som følsomt",
|
||||
"compose_form.spoiler_placeholder": "Innholdsadvarsel",
|
||||
"compose_form.spoiler": "Skjul tekst bak advarsel",
|
||||
"compose_form.private": "Merk som privat",
|
||||
"compose_form.privacy_disclaimer": "Din private status vil leveres til nevnte brukere på {domains}. Stoler du på {domainsCount, plural, one {den serveren} other {de serverne}}? Synlighet fungerer kun på Mastodon-instanser. Hvis {domains} {domainsCount, plural, one {ike er en Mastodon-instans} other {ikke er Mastodon-instanser}}, vil det ikke indikeres at posten din er privat, og den kan kanskje bli reblogget eller på annen måte bli synlig for uventede mottakere.",
|
||||
"compose_form.unlisted": "Ikke vis på offentlige tidslinjer",
|
||||
"navigation_bar.edit_profile": "Rediger profil",
|
||||
"navigation_bar.preferences": "Preferanser",
|
||||
"navigation_bar.community_timeline": "Lokal tidslinje",
|
||||
"navigation_bar.public_timeline": "Forent tidslinje",
|
||||
"navigation_bar.logout": "Logg ut",
|
||||
"emoji_button.label": "Sett inn emoji",
|
||||
"empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!",
|
||||
"empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.",
|
||||
"empty_column.home.public_timeline": "en offentlig tidslinje",
|
||||
"empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.",
|
||||
"empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.",
|
||||
"empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp",
|
||||
"follow_request.authorize": "Autorisér",
|
||||
"follow_request.reject": "Avvis",
|
||||
"getting_started.apps": "Diverse apper er tilgjengelige",
|
||||
"getting_started.heading": "Kom i gang",
|
||||
"getting_started.open_source_notice": "Mastodon er fri programvare. Du kan bidra eller rapportere problemer på GitHub på {github}. {apps}.",
|
||||
"home.column_settings.advanced": "Advansert",
|
||||
"home.column_settings.basic": "Enkel",
|
||||
"home.column_settings.filter_regex": "Filtrér med regulære uttrykk",
|
||||
"home.column_settings.show_reblogs": "Vis fremhevinger",
|
||||
"home.column_settings.show_replies": "Vis svar",
|
||||
"home.settings": "Kolonneinnstillinger",
|
||||
"lightbox.close": "Lukk",
|
||||
"loading_indicator.label": "Laster...",
|
||||
"media_gallery.toggle_visible": "Veksle synlighet",
|
||||
"missing_indicator.label": "Ikke funnet",
|
||||
"navigation_bar.blocks": "Blokkerte brukere",
|
||||
"navigation_bar.info": "Utvidet informasjon",
|
||||
"navigation_bar.community_timeline": "Lokal tidslinje",
|
||||
"navigation_bar.edit_profile": "Rediger profil",
|
||||
"navigation_bar.favourites": "Likt",
|
||||
"navigation_bar.follow_requests": "Følgeforespørsler",
|
||||
"navigation_bar.info": "Utvidet informasjon",
|
||||
"navigation_bar.logout": "Logg ut",
|
||||
"navigation_bar.preferences": "Preferanser",
|
||||
"navigation_bar.public_timeline": "Felles tidslinje",
|
||||
"notification.favourite": "{name} likte din status",
|
||||
"notification.follow": "{name} fulgte deg",
|
||||
"notification.reblog": "{name} fremhevde din status",
|
||||
"notifications.clear_confirmation": "Er du sikker på at du vil fjerne alle dine varsler?",
|
||||
"notifications.clear": "Fjern varsler",
|
||||
"notifications.column_settings.alert": "Skrivebordsvarslinger",
|
||||
"notifications.column_settings.favourite": "Likt:",
|
||||
"notifications.column_settings.follow": "Nye følgere:",
|
||||
"notifications.column_settings.mention": "Nevninger:",
|
||||
"notifications.column_settings.reblog": "Fremhevinger:",
|
||||
"notifications.column_settings.show": "Vis i kolonne",
|
||||
"notifications.column_settings.sound": "Spill lyd",
|
||||
"notifications.settings": "Kolonneinstillinger",
|
||||
"privacy.change": "Justér synlighet",
|
||||
"privacy.direct.long": "Post kun til nevnte brukere",
|
||||
"privacy.direct.short": "Direkte",
|
||||
"privacy.private.long": "Post kun til følgere",
|
||||
"privacy.private.short": "Privat",
|
||||
"privacy.public.long": "Post kun til offentlige tidslinjer",
|
||||
"privacy.public.short": "Offentlig",
|
||||
"privacy.unlisted.long": "Ikke vis i offentlige tidslinjer",
|
||||
"privacy.unlisted.short": "Uoppført",
|
||||
"reply_indicator.cancel": "Avbryt",
|
||||
"report.heading": "Ny rapport",
|
||||
"report.placeholder": "Tilleggskommentarer",
|
||||
"report.submit": "Send inn",
|
||||
"report.target": "Rapporterer",
|
||||
"search_results.total": "{count} {count, plural, one {resultat} other {resultater}}",
|
||||
"search.placeholder": "Søk",
|
||||
"search.account": "Konto",
|
||||
"search.hashtag": "Hashtag",
|
||||
"search.status_by": "Status fra {name}",
|
||||
"status.delete": "Slett",
|
||||
"status.favourite": "Lik",
|
||||
"status.load_more": "Last mer",
|
||||
"status.media_hidden": "Media skjult",
|
||||
"status.mention": "Nevn @{name}",
|
||||
"status.open": "Utvid denne statusen",
|
||||
"status.reblog": "Fremhev",
|
||||
"status.reblogged_by": "Fremhevd av {name}",
|
||||
"status.reply": "Svar",
|
||||
"status.report": "Rapporter @{name}",
|
||||
"status.sensitive_toggle": "Klikk for å vise",
|
||||
"status.sensitive_warning": "Følsomt innhold",
|
||||
"status.show_less": "Vis mindre",
|
||||
"status.show_more": "Vis mer",
|
||||
"tabs_bar.compose": "Komponer",
|
||||
"tabs_bar.federated_timeline": "Felles",
|
||||
"tabs_bar.home": "Hjem",
|
||||
"tabs_bar.local_timeline": "Lokal",
|
||||
"tabs_bar.notifications": "Varslinger",
|
||||
"upload_area.title": "Dra og slipp for å laste opp",
|
||||
"upload_button.label": "Legg til media",
|
||||
"upload_form.undo": "Angre",
|
||||
"notification.follow": "{name} fulgte deg",
|
||||
"notification.favourite": "{name} likte din status",
|
||||
"notification.reblog": "{name} reblogget din status",
|
||||
"notification.mention": "{name} nevnte deg",
|
||||
"notifications.column_settings.alert": "Skrivebordsvarslinger",
|
||||
"notifications.column_settings.show": "Vis i kolonne",
|
||||
"notifications.column_settings.follow": "Nye følgere:",
|
||||
"notifications.column_settings.favourite": "Likt:",
|
||||
"notifications.column_settings.mention": "Nevninger:",
|
||||
"notifications.column_settings.reblog": "Reblogginger:",
|
||||
"upload_progress.label": "Laster opp...",
|
||||
"video_player.toggle_sound": "Veksle lyd",
|
||||
"video_player.toggle_visible": "Veksle synlighet",
|
||||
"video_player.expand": "Utvid video",
|
||||
"getting_started.about_addressing": "Du kan følge noen hvis du vet brukernavnet deres og domenet de er på ved å skrive en e-postadresse inn i søkeskjemaet.",
|
||||
"getting_started.about_shortcuts": "Hvis målbrukeren er på samme domene som deg, vil kun brukernavnet også fungere. Den samme regelen gjelder når man nevner noen i statuser.",
|
||||
"tabs_bar.mentions": "Nevninger",
|
||||
"tabs_bar.public": "Felles tidslinje",
|
||||
"compose_form.private": "Merk som privat",
|
||||
"compose_form.unlisted": "Ikke vis på offentlige tidslinjer",
|
||||
"search.account": "Konto",
|
||||
"search.hashtag": "Hashtag",
|
||||
"notification.mention": "{name} nevnte deg"
|
||||
};
|
||||
|
||||
export default no;
|
||||
|
|
|
@ -2,7 +2,9 @@ const ru = {
|
|||
"column_back_button.label": "Назад",
|
||||
"lightbox.close": "Закрыть",
|
||||
"loading_indicator.label": "Загрузка...",
|
||||
"missing_indicator.label": "Не найдено",
|
||||
"status.mention": "Упомянуть @{name}",
|
||||
"status.media_hidden": "Медиаконтент скрыт",
|
||||
"status.delete": "Удалить",
|
||||
"status.reply": "Ответить",
|
||||
"status.reblog": "Продвинуть",
|
||||
|
@ -16,18 +18,23 @@ const ru = {
|
|||
"status.report": "Пожаловаться",
|
||||
"status.load_more": "Показать еще",
|
||||
"video_player.toggle_sound": "Вкл./выкл. звук",
|
||||
"video_player.toggle_visible": "Показать/скрыть",
|
||||
"account.disclaimer": "Это пользователь с другого узла. Число может быть больше.",
|
||||
"account.mention": "Упомянуть",
|
||||
"account.edit_profile": "Изменить профиль",
|
||||
"account.unblock": "Разблокировать",
|
||||
"account.unfollow": "Отписаться",
|
||||
"account.block": "Блокировать",
|
||||
"account.mute": "Заглушить",
|
||||
"account.report": "Пожаловаться",
|
||||
"account.unmute": "Снять глушение",
|
||||
"account.follow": "Подписаться",
|
||||
"account.posts": "Посты",
|
||||
"account.follows": "Подписки",
|
||||
"account.followers": "Подписаны",
|
||||
"account.follows_you": "Подписан(а) на Вас",
|
||||
"account.requested": "Ожидает подтверждения",
|
||||
"boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
|
||||
"getting_started.heading": "Добро пожаловать",
|
||||
"getting_started.about_addressing": "Вы можете подписаться на человека, зная имя пользователя и домен, на котором он находится, введя e-mail-подобный адрес в форму поиска.",
|
||||
"getting_started.about_shortcuts": "Если пользователь находится на одном с Вами домене, можно использовать только имя. То же правило применимо к упоминанию пользователей в статусах.",
|
||||
|
@ -37,11 +44,16 @@ const ru = {
|
|||
"column.community": "Локальная лента",
|
||||
"column.public": "Глобальная лента",
|
||||
"column.notifications": "Уведомления",
|
||||
"column.favourites": "Понравившееся",
|
||||
"column.blocks": "Список блокировки",
|
||||
"column.follow_requests": "Запросы на подписку",
|
||||
"tabs_bar.compose": "Написать",
|
||||
"tabs_bar.home": "Главная",
|
||||
"tabs_bar.mentions": "Упоминания",
|
||||
"tabs_bar.public": "Глобальная лента",
|
||||
"tabs_bar.notifications": "Уведомления",
|
||||
"tabs_bar.local_timeline": "Локальная",
|
||||
"tabs_bar.federated_timeline": "Глобальная",
|
||||
"compose_form.placeholder": "О чем Вы думаете?",
|
||||
"compose_form.publish": "Трубить",
|
||||
"compose_form.sensitive": "Отметить как чувствительный контент",
|
||||
|
@ -49,6 +61,7 @@ const ru = {
|
|||
"compose_form.private": "Отметить как приватное",
|
||||
"compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.",
|
||||
"compose_form.unlisted": "Не отображать в публичных лентах",
|
||||
"compose_form.spoiler_placeholder": "Не для всех",
|
||||
"navigation_bar.edit_profile": "Изменить профиль",
|
||||
"navigation_bar.preferences": "Опции",
|
||||
"navigation_bar.community_timeline": "Локальная лента",
|
||||
|
@ -57,12 +70,20 @@ const ru = {
|
|||
"navigation_bar.info": "Об узле",
|
||||
"navigation_bar.favourites": "Понравившееся",
|
||||
"navigation_bar.blocks": "Список блокировки",
|
||||
"navigation_bar.follow_requests": "Запросы на подписку",
|
||||
"reply_indicator.cancel": "Отмена",
|
||||
"report.target": "Жалуемся на",
|
||||
"report.heading": "Новая жалоба",
|
||||
"report.placeholder": "Комментарий",
|
||||
"report.submit": "Отправить",
|
||||
"search.placeholder": "Поиск",
|
||||
"search.account": "Аккаунт",
|
||||
"search.hashtag": "Хэштег",
|
||||
"search.status_by": "Статус от {name}",
|
||||
"upload_area.title": "Перетащите сюда, чтобы загрузить",
|
||||
"upload_button.label": "Добавить медиаконтент",
|
||||
"upload_form.undo": "Отменить",
|
||||
"upload_progress.label": "Загрузка...",
|
||||
"notification.follow": "{name} подписался(-лась) на Вас",
|
||||
"notification.favourite": "{name} понравился Ваш статус",
|
||||
"notification.reblog": "{name} продвинул(а) Ваш статус",
|
||||
|
@ -71,9 +92,10 @@ const ru = {
|
|||
"home.column_settings.basic": "Основные",
|
||||
"home.column_settings.advanced": "Дополнительные",
|
||||
"home.column_settings.filter_regex": "Отфильтровать регулярным выражением",
|
||||
"home.column_settings.show_replies": "Показывать продвижения",
|
||||
"home.column_settings.show_reblogs": "Показывать продвижения",
|
||||
"home.column_settings.show_replies": "Показывать ответы",
|
||||
"notifications.clear": "Очистить уведомления",
|
||||
"notifications.clear_confirmation": "Вы уверены, что хотите очистить все уведомления?",
|
||||
"notifications.settings": "Настройки колонки",
|
||||
"notifications.column_settings.alert": "Десктопные уведомления",
|
||||
"notifications.column_settings.show": "Показывать в колонке",
|
||||
|
@ -96,6 +118,10 @@ const ru = {
|
|||
"privacy.private.long": "Показать только подписчикам",
|
||||
"privacy.direct.short": "Направленный",
|
||||
"privacy.direct.long": "Показать только упомянутым",
|
||||
"emoji_button.label": "Вставить эмодзи",
|
||||
"follow_request.authorize": "Авторизовать",
|
||||
"follow_request.reject": "Отказать",
|
||||
"media_gallery.toggle_visible": "Показать/скрыть",
|
||||
};
|
||||
|
||||
export default ru;
|
||||
|
|
|
@ -19,19 +19,27 @@ export { localeData as localeData };
|
|||
|
||||
const zh_hk = {
|
||||
"account.block": "封鎖 @{name}",
|
||||
"account.disclaimer": "由於這個用戶在另一個服務站,實際數字會比這個更多。",
|
||||
"account.edit_profile": "修改個人資料",
|
||||
"account.follow": "關注",
|
||||
"account.followers": "關注的人",
|
||||
"account.follows_you": "關注你",
|
||||
"account.follows": "正在關注",
|
||||
"account.mention": "提及 @{name}",
|
||||
"account.mute": "將 @{name} 靜音",
|
||||
"account.posts": "文章",
|
||||
"account.report": "舉報 @{name}",
|
||||
"account.requested": "等候審批",
|
||||
"account.unblock": "解除對 @{name} 的封鎖",
|
||||
"account.unfollow": "取消關注",
|
||||
"column_back_button.label": "先前顯示",
|
||||
"account.unmute": "取消 @{name} 的靜音",
|
||||
"boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
|
||||
"column_back_button.label": "返回",
|
||||
"column.blocks": "封鎖用戶",
|
||||
"column.community": "本站時間軸",
|
||||
"column.home": "家",
|
||||
"column.favourites": "喜歡的文章",
|
||||
"column.follow_requests": "關注請求",
|
||||
"column.home": "主頁",
|
||||
"column.notifications": "通知",
|
||||
"column.public": "跨站公共時間軸",
|
||||
"compose_form.placeholder": "你在想甚麼?",
|
||||
|
@ -39,35 +47,49 @@ const zh_hk = {
|
|||
"compose_form.private": "標示為「只有關注你的人能看」",
|
||||
"compose_form.publish": "發文",
|
||||
"compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
|
||||
"compose_form.spoiler_placeholder": "敏感內容",
|
||||
"compose_form.spoiler": "將部份文字藏於警告訊息之後",
|
||||
"compose_form.unlisted": "請勿在公共時間軸顯示",
|
||||
"emoji_button.label": "加入表情符號",
|
||||
"empty_column.community": "本站時間軸暫時未有內容,快貼文來搶頭香啊!",
|
||||
"empty_column.hashtag": "這個標籤暫時未有內容。",
|
||||
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
|
||||
"empty_column.home.public_timeline": "公共時間軸",
|
||||
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
|
||||
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up.",
|
||||
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
|
||||
"empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。",
|
||||
"empty_column.public": "跨站公共時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。",
|
||||
"follow_request.authorize": "批准",
|
||||
"follow_request.reject": "拒絕",
|
||||
"getting_started.about_addressing": "只要你知道一位用戶的用戶名稱和域名,你可以用「@用戶名稱@域名」的格式在搜尋欄尋找該用戶。",
|
||||
"getting_started.about_shortcuts": "只要該用戶是在你現在的服務站開立,你可以直接輸入用戶𠱷搜尋。同樣的規則適用於在文章提及別的用戶。",
|
||||
"getting_started.apps": "手機或桌面應用程式",
|
||||
"getting_started.heading": "開始使用",
|
||||
"getting_started.open_source_notice": "Mastodon 是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。你亦可透過{apps}閱讀 Mastodon 上的消息。",
|
||||
"home.column_settings.advanced": "進階",
|
||||
"home.column_settings.basic": "基本",
|
||||
"home.column_settings.filter_regex": "使用正規表達式 (regular expression) 過濾",
|
||||
"home.column_settings.show_reblogs": "顯示被轉推的文章",
|
||||
"home.column_settings.show_replies": "顯示回應文章",
|
||||
"home.column_settings.advanced": "進階",
|
||||
"lightbox.close": "關閉",
|
||||
"home.settings": "欄位設定",
|
||||
"lightbox.close": "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.preferences": "個人設定",
|
||||
"navigation_bar.preferences": "偏好設定",
|
||||
"navigation_bar.public_timeline": "跨站公共時間軸",
|
||||
"notification.favourite": "{name} 喜歡你的文章",
|
||||
"notification.follow": "{name} 開始開始你",
|
||||
"notification.follow": "{name} 開始關注你",
|
||||
"notification.mention": "{name} 提及你",
|
||||
"notification.reblog": "{name} 轉推你的文章",
|
||||
"notifications.clear_confirmation": "你確定要清空通知紀錄嗎?",
|
||||
"notifications.clear": "清空通知紀錄",
|
||||
"notifications.column_settings.alert": "顯示桌面通知",
|
||||
"notifications.column_settings.favourite": "喜歡你的文章:",
|
||||
"notifications.column_settings.follow": "關注你:",
|
||||
|
@ -75,13 +97,26 @@ const zh_hk = {
|
|||
"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": "在公共時間軸顯示",
|
||||
"privacy.public.short": "公共",
|
||||
"privacy.unlisted.long": "公開,但不在公共時間軸顯示",
|
||||
"privacy.unlisted.short": "公開",
|
||||
"reply_indicator.cancel": "取消",
|
||||
"report.heading": "舉報",
|
||||
"report.placeholder": "額外訊息",
|
||||
"report.submit": "提交",
|
||||
"report.target": "Reporting",
|
||||
"search_results.total": "{count} 項結果",
|
||||
"search.account": "用戶",
|
||||
"search.hashtag": "標籤",
|
||||
"search.placeholder": "搜尋",
|
||||
"search_results.total": "{count} 項結果",
|
||||
"search.status_by": "按用戶名稱搜尋文章",
|
||||
"search.status_by": "按{name}搜尋文章",
|
||||
"status.delete": "刪除",
|
||||
"status.favourite": "喜歡",
|
||||
"status.load_more": "載入更多",
|
||||
|
@ -97,17 +132,19 @@ const zh_hk = {
|
|||
"status.show_less": "減少顯示",
|
||||
"status.show_more": "顯示更多",
|
||||
"tabs_bar.compose": "撰寫",
|
||||
"tabs_bar.home": "家",
|
||||
"tabs_bar.federated_timeline": "跨站",
|
||||
"tabs_bar.home": "主頁",
|
||||
"tabs_bar.local_timeline": "本站",
|
||||
"tabs_bar.mentions": "提及",
|
||||
"tabs_bar.notifications": "通知",
|
||||
"tabs_bar.public": "跨站公共時間軸",
|
||||
"tabs_bar.federated_timeline": "跨站",
|
||||
"upload_area.title": "將檔案拖放至此上載",
|
||||
"upload_button.label": "上載媒體檔案",
|
||||
"upload_progress.label": "上載中……",
|
||||
"upload_form.undo": "還原",
|
||||
"upload_progress.label": "上載中……",
|
||||
"video_player.expand": "展開影片",
|
||||
"video_player.toggle_sound": "開關音效",
|
||||
"video_player.toggle_visible": "打開或關上",
|
||||
};
|
||||
|
||||
export default zh_hk;
|
||||
|
|
|
@ -22,7 +22,7 @@ export default function errorsMiddleware() {
|
|||
|
||||
dispatch(showAlert(title, message));
|
||||
} else {
|
||||
console.error(action.error);
|
||||
console.error(action.error); // eslint-disable-line no-console
|
||||
dispatch(showAlert('Oops!', 'An unexpected error occurred.'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,10 @@ import {
|
|||
BLOCKS_FETCH_SUCCESS,
|
||||
BLOCKS_EXPAND_SUCCESS
|
||||
} from '../actions/blocks';
|
||||
import {
|
||||
MUTES_FETCH_SUCCESS,
|
||||
MUTES_EXPAND_SUCCESS
|
||||
} from '../actions/mutes';
|
||||
import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
|
||||
import {
|
||||
REBLOG_SUCCESS,
|
||||
|
@ -94,6 +98,8 @@ export default function accounts(state = initialState, action) {
|
|||
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
||||
case BLOCKS_FETCH_SUCCESS:
|
||||
case BLOCKS_EXPAND_SUCCESS:
|
||||
case MUTES_FETCH_SUCCESS:
|
||||
case MUTES_EXPAND_SUCCESS:
|
||||
return normalizeAccounts(state, action.accounts);
|
||||
case NOTIFICATIONS_REFRESH_SUCCESS:
|
||||
case NOTIFICATIONS_EXPAND_SUCCESS:
|
||||
|
|
|
@ -9,17 +9,17 @@ const initialState = Immutable.List([]);
|
|||
|
||||
export default function alerts(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case ALERT_SHOW:
|
||||
return state.push(Immutable.Map({
|
||||
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
||||
title: action.title,
|
||||
message: action.message
|
||||
}));
|
||||
case ALERT_DISMISS:
|
||||
return state.filterNot(item => item.get('key') === action.alert.key);
|
||||
case ALERT_CLEAR:
|
||||
return state.clear();
|
||||
default:
|
||||
return state;
|
||||
case ALERT_SHOW:
|
||||
return state.push(Immutable.Map({
|
||||
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
||||
title: action.title,
|
||||
message: action.message
|
||||
}));
|
||||
case ALERT_DISMISS:
|
||||
return state.filterNot(item => item.get('key') === action.alert.key);
|
||||
case ALERT_CLEAR:
|
||||
return state.clear();
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@ import { STORE_HYDRATE } from '../actions/store';
|
|||
import Immutable from 'immutable';
|
||||
|
||||
const initialState = Immutable.Map({
|
||||
streaming_api_base_url: null,
|
||||
access_token: null,
|
||||
me: null
|
||||
});
|
||||
|
|
|
@ -16,6 +16,10 @@ import {
|
|||
BLOCKS_FETCH_SUCCESS,
|
||||
BLOCKS_EXPAND_SUCCESS
|
||||
} from '../actions/blocks';
|
||||
import {
|
||||
MUTES_FETCH_SUCCESS,
|
||||
MUTES_EXPAND_SUCCESS
|
||||
} from '../actions/mutes';
|
||||
import Immutable from 'immutable';
|
||||
|
||||
const initialState = Immutable.Map({
|
||||
|
@ -24,7 +28,8 @@ const initialState = Immutable.Map({
|
|||
reblogged_by: Immutable.Map(),
|
||||
favourited_by: Immutable.Map(),
|
||||
follow_requests: Immutable.Map(),
|
||||
blocks: Immutable.Map()
|
||||
blocks: Immutable.Map(),
|
||||
mutes: Immutable.Map()
|
||||
});
|
||||
|
||||
const normalizeList = (state, type, id, accounts, next) => {
|
||||
|
@ -65,6 +70,10 @@ export default function userLists(state = initialState, action) {
|
|||
return state.setIn(['blocks', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
|
||||
case BLOCKS_EXPAND_SUCCESS:
|
||||
return state.updateIn(['blocks', 'items'], list => list.push(...action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
|
||||
case MUTES_FETCH_SUCCESS:
|
||||
return state.setIn(['mutes', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||
case MUTES_EXPAND_SUCCESS:
|
||||
return state.updateIn(['mutes', 'items'], list => list.push(...action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ const createWebSocketURL = (url) => {
|
|||
return a.href;
|
||||
};
|
||||
|
||||
export default function getStream(accessToken, stream, { connected, received, disconnected, reconnected }) {
|
||||
const ws = new WebSocketClient(`${createWebSocketURL(STREAMING_API_BASE_URL)}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
|
||||
export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
|
||||
const ws = new WebSocketClient(`${createWebSocketURL(streamingAPIBaseURL)}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
|
||||
|
||||
ws.onopen = connected;
|
||||
ws.onmessage = e => received(JSON.parse(e.data));
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
p, li {
|
||||
font: 16px/28px 'Montserrat', sans-serif;
|
||||
font-weight: 400;
|
||||
margin-bottom: 26px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
a {
|
||||
color: $color4;
|
||||
|
|
|
@ -2056,3 +2056,11 @@ button.icon-button.active i.fa-retweet {
|
|||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-bar {
|
||||
background-color: $color4;
|
||||
height: 3px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
|
|
@ -6,3 +6,12 @@
|
|||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.recovery-codes {
|
||||
column-count: 2;
|
||||
height: 100px;
|
||||
li {
|
||||
list-style: decimal;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
|
18
app/controllers/admin/resets_controller.rb
Normal file
18
app/controllers/admin/resets_controller.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class ResetsController < BaseController
|
||||
before_action :set_account
|
||||
|
||||
def create
|
||||
@account.user.send_reset_password_instructions
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -14,7 +14,7 @@ class Api::OEmbedController < ApiController
|
|||
def stream_entry_from_url(url)
|
||||
params = Rails.application.routes.recognize_path(url)
|
||||
|
||||
raise ActiveRecord::NotFound unless params[:controller] == 'stream_entries' && params[:action] == 'show'
|
||||
raise ActiveRecord::RecordNotFound unless params[:controller] == 'stream_entries' && params[:action] == 'show'
|
||||
|
||||
StreamEntry.find(params[:id])
|
||||
end
|
||||
|
|
|
@ -30,7 +30,7 @@ class Api::PushController < ApiController
|
|||
params = Rails.application.routes.recognize_path(uri.path)
|
||||
domain = uri.host + (uri.port ? ":#{uri.port}" : '')
|
||||
|
||||
return unless TagManager.instance.local_domain?(domain) && params[:controller] == 'accounts' && params[:action] == 'show' && params[:format] == 'atom'
|
||||
return unless TagManager.instance.web_domain?(domain) && params[:controller] == 'accounts' && params[:action] == 'show' && params[:format] == 'atom'
|
||||
|
||||
Account.find_local(params[:username])
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ class ApplicationController < ActionController::Base
|
|||
force_ssl if: "Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'"
|
||||
|
||||
include Localized
|
||||
helper_method :current_account
|
||||
helper_method :current_account, :single_user_mode?
|
||||
|
||||
rescue_from ActionController::RoutingError, with: :not_found
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
||||
|
@ -69,6 +69,10 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
def single_user_mode?
|
||||
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.first
|
||||
end
|
||||
|
||||
def current_account
|
||||
@current_account ||= current_user.try(:account)
|
||||
end
|
||||
|
|
|
@ -28,7 +28,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
end
|
||||
|
||||
def check_enabled_registrations
|
||||
redirect_to root_path if Rails.configuration.x.single_user_mode || !Setting.open_registrations
|
||||
redirect_to root_path if single_user_mode? || !Setting.open_registrations
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -49,7 +49,8 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
end
|
||||
|
||||
def valid_otp_attempt?(user)
|
||||
user.validate_and_consume_otp!(user_params[:otp_attempt])
|
||||
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
|
||||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
|
||||
end
|
||||
|
||||
def authenticate_with_two_factor
|
||||
|
|
|
@ -27,7 +27,11 @@ module Localized
|
|||
|
||||
def default_locale
|
||||
ENV.fetch('DEFAULT_LOCALE') {
|
||||
http_accept_language.compatible_language_from(I18n.available_locales) || I18n.default_locale
|
||||
user_supplied_locale || I18n.default_locale
|
||||
}
|
||||
end
|
||||
|
||||
def user_supplied_locale
|
||||
http_accept_language.language_region_compatible_from(I18n.available_locales)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,15 +4,16 @@ class HomeController < ApplicationController
|
|||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@body_classes = 'app-body'
|
||||
@token = find_or_create_access_token.token
|
||||
@web_settings = Web::Setting.find_by(user: current_user)&.data || {}
|
||||
@body_classes = 'app-body'
|
||||
@token = find_or_create_access_token.token
|
||||
@web_settings = Web::Setting.find_by(user: current_user)&.data || {}
|
||||
@streaming_api_base_url = Rails.configuration.x.streaming_api_base_url
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authenticate_user!
|
||||
redirect_to(Rails.configuration.x.single_user_mode ? account_path(Account.first) : about_path) unless user_signed_in?
|
||||
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
|
||||
end
|
||||
|
||||
def find_or_create_access_token
|
||||
|
|
|
@ -19,9 +19,9 @@ class Settings::TwoFactorAuthsController < ApplicationController
|
|||
def create
|
||||
if current_user.validate_and_consume_otp!(confirmation_params[:code])
|
||||
current_user.otp_required_for_login = true
|
||||
@codes = current_user.generate_otp_backup_codes!
|
||||
current_user.save!
|
||||
|
||||
redirect_to settings_two_factor_auth_path, notice: I18n.t('two_factor_auth.enabled_success')
|
||||
flash[:notice] = I18n.t('two_factor_auth.enabled_success')
|
||||
else
|
||||
@confirmation = Form::TwoFactorConfirmation.new
|
||||
set_qr_code
|
||||
|
@ -30,6 +30,12 @@ class Settings::TwoFactorAuthsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def recovery_codes
|
||||
@codes = current_user.generate_otp_backup_codes!
|
||||
current_user.save!
|
||||
flash[:notice] = I18n.t('two_factor_auth.recovery_codes_regenerated')
|
||||
end
|
||||
|
||||
def disable
|
||||
current_user.otp_required_for_login = false
|
||||
current_user.save!
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin::AccountsHelper
|
||||
def filter_params(more_params)
|
||||
params.permit(:local, :remote, :by_domain, :silenced, :suspended, :recent, :resolved).merge(more_params)
|
||||
end
|
||||
module Admin::FilterHelper
|
||||
ACCOUNT_FILTERS = %i[local remote by_domain silenced suspended recent].freeze
|
||||
REPORT_FILTERS = %i[resolved].freeze
|
||||
|
||||
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS
|
||||
|
||||
def filter_link_to(text, more_params)
|
||||
new_url = filtered_url_for(more_params)
|
||||
|
@ -16,6 +17,10 @@ module Admin::AccountsHelper
|
|||
|
||||
private
|
||||
|
||||
def filter_params(more_params)
|
||||
params.permit(FILTERS).merge(more_params)
|
||||
end
|
||||
|
||||
def filter_link_class(new_url)
|
||||
filtered_url_for(params) == new_url ? 'selected' : ''
|
||||
end
|
|
@ -56,6 +56,10 @@ class TagManager
|
|||
id.start_with?("tag:#{Rails.configuration.x.local_domain}")
|
||||
end
|
||||
|
||||
def web_domain?(domain)
|
||||
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.web_domain).zero?
|
||||
end
|
||||
|
||||
def local_domain?(domain)
|
||||
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.local_domain).zero?
|
||||
end
|
||||
|
|
|
@ -5,7 +5,9 @@ class User < ApplicationRecord
|
|||
|
||||
devise :registerable, :recoverable,
|
||||
:rememberable, :trackable, :validatable, :confirmable,
|
||||
:two_factor_authenticatable, otp_secret_encryption_key: ENV['OTP_SECRET']
|
||||
:two_factor_authenticatable, :two_factor_backupable,
|
||||
otp_secret_encryption_key: ENV['OTP_SECRET'],
|
||||
otp_number_of_backup_codes: 10
|
||||
|
||||
belongs_to :account, inverse_of: :user
|
||||
accepts_nested_attributes_for :account
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class InstancePresenter
|
||||
delegate(
|
||||
:closed_registrations_message,
|
||||
:contact_email,
|
||||
:site_contact_email,
|
||||
:open_registrations,
|
||||
:site_description,
|
||||
:site_extended_description,
|
||||
|
|
|
@ -1,26 +1,86 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AccountSearchService < BaseService
|
||||
attr_reader :query, :limit, :resolve, :account
|
||||
|
||||
def call(query, limit, resolve = false, account = nil)
|
||||
return [] if query.blank? || query.start_with?('#')
|
||||
@query = query
|
||||
@limit = limit
|
||||
@resolve = resolve
|
||||
@account = account
|
||||
|
||||
username, domain = query.gsub(/\A@/, '').split('@')
|
||||
domain = nil if TagManager.instance.local_domain?(domain)
|
||||
search_service_results
|
||||
end
|
||||
|
||||
if domain.nil?
|
||||
exact_match = Account.find_local(username)
|
||||
results = account.nil? ? Account.search_for(username, limit) : Account.advanced_search_for(username, account, limit)
|
||||
private
|
||||
|
||||
def search_service_results
|
||||
return [] if query_blank_or_hashtag?
|
||||
|
||||
if resolving_non_matching_remote_account?
|
||||
[FollowRemoteAccountService.new.call("#{query_username}@#{query_domain}")]
|
||||
else
|
||||
exact_match = Account.find_remote(username, domain)
|
||||
results = account.nil? ? Account.search_for("#{username} #{domain}", limit) : Account.advanced_search_for("#{username} #{domain}", account, limit)
|
||||
search_results_and_exact_match.compact.uniq
|
||||
end
|
||||
end
|
||||
|
||||
results = [exact_match] + results.reject { |a| a.id == exact_match.id } if exact_match
|
||||
def resolving_non_matching_remote_account?
|
||||
resolve && !exact_match && !domain_is_local?
|
||||
end
|
||||
|
||||
if resolve && !exact_match && !domain.nil?
|
||||
results = [FollowRemoteAccountService.new.call("#{username}@#{domain}")]
|
||||
def search_results_and_exact_match
|
||||
[exact_match] + search_results.to_a
|
||||
end
|
||||
|
||||
def query_blank_or_hashtag?
|
||||
query.blank? || query.start_with?('#')
|
||||
end
|
||||
|
||||
def split_query_string
|
||||
@_split_query_string ||= query.gsub(/\A@/, '').split('@')
|
||||
end
|
||||
|
||||
def query_username
|
||||
@_query_username ||= split_query_string.first
|
||||
end
|
||||
|
||||
def query_domain
|
||||
@_query_domain ||= query_without_split? ? nil : split_query_string.last
|
||||
end
|
||||
|
||||
def query_without_split?
|
||||
split_query_string.size == 1
|
||||
end
|
||||
|
||||
def domain_is_local?
|
||||
@_domain_is_local ||= TagManager.instance.local_domain?(query_domain)
|
||||
end
|
||||
|
||||
def exact_match
|
||||
@_exact_match ||= Account.find_remote(query_username, query_domain)
|
||||
end
|
||||
|
||||
def search_results
|
||||
@_search_results ||= if account
|
||||
advanced_search_results
|
||||
else
|
||||
simple_search_results
|
||||
end
|
||||
end
|
||||
|
||||
results
|
||||
def advanced_search_results
|
||||
Account.advanced_search_for(terms_for_query, account, limit)
|
||||
end
|
||||
|
||||
def simple_search_results
|
||||
Account.search_for(terms_for_query, limit)
|
||||
end
|
||||
|
||||
def terms_for_query
|
||||
if domain_is_local?
|
||||
query_username
|
||||
else
|
||||
"#{query_username} #{query_domain}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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.nil?
|
||||
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}"
|
||||
|
||||
|
@ -29,20 +29,24 @@ class FollowRemoteAccountService < BaseService
|
|||
return Account.find_local(confirmed_username) if TagManager.instance.local_domain?(confirmed_domain)
|
||||
|
||||
confirmed_account = Account.find_remote(confirmed_username, confirmed_domain)
|
||||
return confirmed_account unless confirmed_account.nil?
|
||||
if confirmed_account.nil?
|
||||
Rails.logger.debug "Creating new remote account for #{uri}"
|
||||
|
||||
Rails.logger.debug "Creating new remote account for #{uri}"
|
||||
domain_block = DomainBlock.find_by(domain: domain)
|
||||
account = Account.new(username: confirmed_username, domain: confirmed_domain)
|
||||
account.suspended = true if domain_block && domain_block.suspend?
|
||||
account.silenced = true if domain_block && domain_block.silence?
|
||||
account.private_key = nil
|
||||
else
|
||||
account = confirmed_account
|
||||
end
|
||||
|
||||
domain_block = DomainBlock.find_by(domain: domain)
|
||||
account.last_webfingered_at = Time.now.utc
|
||||
|
||||
account = Account.new(username: confirmed_username, domain: confirmed_domain)
|
||||
account.remote_url = data.link('http://schemas.google.com/g/2010#updates-from').href
|
||||
account.salmon_url = data.link('salmon').href
|
||||
account.url = data.link('http://webfinger.net/rel/profile-page').href
|
||||
account.public_key = magic_key_to_pem(data.link('magic-public-key').href)
|
||||
account.private_key = nil
|
||||
account.suspended = true if domain_block && domain_block.suspend?
|
||||
account.silenced = true if domain_block && domain_block.silence?
|
||||
|
||||
body, xml = get_feed(account.remote_url)
|
||||
hubs = get_hubs(xml)
|
||||
|
|
|
@ -164,7 +164,7 @@ class ProcessFeedService < BaseService
|
|||
|
||||
url = Addressable::URI.parse(link['href'])
|
||||
|
||||
mentioned_account = if TagManager.instance.local_domain?(url.host)
|
||||
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'])
|
||||
|
|
|
@ -4,7 +4,7 @@ class Pubsubhubbub::UnsubscribeService < BaseService
|
|||
def call(account, callback)
|
||||
return ['Invalid topic URL', 422] if account.nil?
|
||||
|
||||
subscription = Subscription.where(account: account, callback_url: callback)
|
||||
subscription = Subscription.find_by(account: account, callback_url: callback)
|
||||
|
||||
unless subscription.nil?
|
||||
Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'unsubscribe')
|
||||
|
|
15
app/views/about/_contact.html.haml
Normal file
15
app/views/about/_contact.html.haml
Normal file
|
@ -0,0 +1,15 @@
|
|||
.panel
|
||||
.panel-header= t 'about.contact'
|
||||
.panel-body
|
||||
- if contact.contact_account
|
||||
.owner
|
||||
.avatar= image_tag contact.contact_account.avatar.url
|
||||
.name
|
||||
= link_to TagManager.instance.url_for(contact.contact_account) do
|
||||
%span.display_name.emojify= display_name(contact.contact_account)
|
||||
%span.username= "@#{contact.contact_account.acct}"
|
||||
|
||||
- if contact.site_contact_email
|
||||
.contact-email
|
||||
= t 'about.business_email'
|
||||
%strong= contact.site_contact_email
|
11
app/views/about/_links.html.haml
Normal file
11
app/views/about/_links.html.haml
Normal file
|
@ -0,0 +1,11 @@
|
|||
.panel
|
||||
.panel-header= t 'about.links'
|
||||
.panel-list
|
||||
%ul
|
||||
- if user_signed_in?
|
||||
%li= link_to t('about.get_started'), root_path
|
||||
- else
|
||||
%li= link_to t('about.get_started'), new_user_registration_path
|
||||
%li= link_to t('auth.login'), new_user_session_path
|
||||
%li= link_to t('about.terms'), terms_path
|
||||
%li= link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon'
|
|
@ -28,29 +28,5 @@
|
|||
.panel= @instance_presenter.site_extended_description.html_safe
|
||||
|
||||
.sidebar
|
||||
.panel
|
||||
.panel-header= t 'about.contact'
|
||||
.panel-body
|
||||
- if @instance_presenter.contact_account
|
||||
.owner
|
||||
.avatar= image_tag @instance_presenter.contact_account.avatar.url
|
||||
.name
|
||||
= link_to TagManager.instance.url_for(@instance_presenter.contact_account) do
|
||||
%span.display_name.emojify= display_name(@instance_presenter.contact_account)
|
||||
%span.username= "@#{@instance_presenter.contact_account.acct}"
|
||||
|
||||
- unless @instance_presenter.contact_email.blank?
|
||||
.contact-email
|
||||
= t 'about.business_email'
|
||||
%strong= @instance_presenter.contact_email
|
||||
.panel
|
||||
.panel-header= t 'about.links'
|
||||
.panel-list
|
||||
%ul
|
||||
- if user_signed_in?
|
||||
%li= link_to t('about.get_started'), root_path
|
||||
- else
|
||||
%li= link_to t('about.get_started'), new_user_registration_path
|
||||
%li= link_to t('auth.login'), new_user_session_path
|
||||
%li= link_to t('about.terms'), terms_path
|
||||
%li= link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon'
|
||||
= render partial: 'contact', object: @instance_presenter
|
||||
= render 'links'
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
.wrapper
|
||||
%h1
|
||||
= image_tag 'logo.png'
|
||||
Mastodon
|
||||
= Setting.site_title
|
||||
|
||||
%p= t('about.about_mastodon').html_safe
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
%meta{ property: 'og:image:height', content: '120' }/
|
||||
%meta{ property: 'twitter:card', content: 'summary' }/
|
||||
|
||||
- if !user_signed_in? && !Rails.configuration.x.single_user_mode
|
||||
- if !user_signed_in? && !single_user_mode?
|
||||
= render partial: 'shared/landing_strip', locals: { account: @account }
|
||||
|
||||
.h-feed
|
||||
|
|
|
@ -61,12 +61,16 @@
|
|||
= surround '(', ')' do
|
||||
= number_to_human_size @account.media_attachments.sum('file_file_size')
|
||||
|
||||
- if @account.silenced?
|
||||
= link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button'
|
||||
- else
|
||||
= link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button'
|
||||
%div{ style: 'float: right' }
|
||||
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button'
|
||||
|
||||
- if @account.suspended?
|
||||
= link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button'
|
||||
- else
|
||||
= link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'
|
||||
%div{ style: 'float: left' }
|
||||
- if @account.silenced?
|
||||
= link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button'
|
||||
- else
|
||||
= link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button'
|
||||
|
||||
- if @account.suspended?
|
||||
= link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button'
|
||||
- else
|
||||
= link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
= t('auth.login')
|
||||
|
||||
= simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
|
||||
= f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt') }, required: true, autofocus: true, autocomplete: 'off'
|
||||
= f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'),
|
||||
input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt') }, required: true, autofocus: true, autocomplete: 'off',
|
||||
hint: t('simple_form.hints.sessions.otp')
|
||||
|
||||
.actions
|
||||
= f.button :button, t('auth.login'), type: :submit
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
- content_for :header_tags do
|
||||
:javascript
|
||||
window.STREAMING_API_BASE_URL = '#{Rails.configuration.x.streaming_api_base_url}';
|
||||
window.INITIAL_STATE = #{json_escape(render(file: 'home/initial_state', formats: :json))}
|
||||
%script#initial-state{:type => 'application/json'}!= json_escape(render(file: 'home/initial_state', formats: :json))
|
||||
|
||||
= javascript_include_tag 'application', integrity: true
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ object false
|
|||
|
||||
node(:meta) do
|
||||
{
|
||||
streaming_api_base_url: @streaming_api_base_url,
|
||||
access_token: @token,
|
||||
locale: I18n.locale,
|
||||
me: current_account.id,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
%p.hint= t('two_factor_auth.recovery_instructions')
|
||||
|
||||
%h3= t('two_factor_auth.recovery_codes')
|
||||
%ol.recovery-codes
|
||||
- @codes.each do |code|
|
||||
%li
|
||||
%samp= code
|
4
app/views/settings/two_factor_auths/create.html.haml
Normal file
4
app/views/settings/two_factor_auths/create.html.haml
Normal file
|
@ -0,0 +1,4 @@
|
|||
- content_for :page_title do
|
||||
= t('settings.two_factor_auth')
|
||||
|
||||
= render 'recovery_codes'
|
|
@ -0,0 +1,4 @@
|
|||
- content_for :page_title do
|
||||
= t('settings.two_factor_auth')
|
||||
|
||||
= render 'recovery_codes'
|
|
@ -8,3 +8,8 @@
|
|||
= link_to t('two_factor_auth.disable'), disable_settings_two_factor_auth_path, data: { method: 'POST' }, class: 'block-button'
|
||||
- else
|
||||
= link_to t('two_factor_auth.setup'), new_settings_two_factor_auth_path, class: 'block-button'
|
||||
|
||||
- if current_user.otp_required_for_login
|
||||
.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'
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
%meta{ property: 'twitter:card', content: 'summary' }/
|
||||
|
||||
- if !user_signed_in? && !Rails.configuration.x.single_user_mode
|
||||
- if !user_signed_in? && !single_user_mode?
|
||||
= render partial: 'shared/landing_strip', locals: { account: @stream_entry.account }
|
||||
|
||||
.activity-stream.activity-stream-headless.h-entry
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
.compact-header
|
||||
%h1<
|
||||
= link_to 'Mastodon', root_path
|
||||
= link_to site_title, root_path
|
||||
%small= "##{@tag.name}"
|
||||
|
||||
- if @statuses.empty?
|
||||
|
|
|
@ -42,6 +42,7 @@ module Mastodon
|
|||
:uk,
|
||||
'zh-CN',
|
||||
:'zh-HK',
|
||||
:'zh-TW',
|
||||
]
|
||||
|
||||
config.i18n.default_locale = :en
|
||||
|
|
|
@ -55,6 +55,8 @@ Rails.application.configure do
|
|||
ENV['REDIS_HOST'] = redis_url.host
|
||||
ENV['REDIS_PORT'] = redis_url.port.to_s
|
||||
ENV['REDIS_PASSWORD'] = redis_url.password
|
||||
db_num = redis_url.path[1..-1]
|
||||
ENV['REDIS_DB'] = db_num if db_num.present?
|
||||
end
|
||||
|
||||
# Use a different cache store in production.
|
||||
|
@ -62,7 +64,7 @@ Rails.application.configure do
|
|||
host: ENV.fetch('REDIS_HOST') { 'localhost' },
|
||||
port: ENV.fetch('REDIS_PORT') { 6379 },
|
||||
password: ENV.fetch('REDIS_PASSWORD') { false },
|
||||
db: 0,
|
||||
db: ENV.fetch('REDIS_DB') { 0 },
|
||||
namespace: 'cache',
|
||||
expires_in: 10.minutes,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
Devise.setup do |config|
|
||||
config.warden do |manager|
|
||||
manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
|
||||
manager.default_strategies(scope: :user).unshift :two_factor_backupable
|
||||
end
|
||||
|
||||
# The secret key used by Devise. Devise uses this key to generate
|
||||
|
|
|
@ -2,18 +2,20 @@
|
|||
|
||||
port = ENV.fetch('PORT') { 3000 }
|
||||
host = ENV.fetch('LOCAL_DOMAIN') { "localhost:#{port}" }
|
||||
web_host = ENV.fetch('WEB_DOMAIN') { host }
|
||||
https = ENV['LOCAL_HTTPS'] == 'true'
|
||||
|
||||
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'
|
||||
|
||||
config.action_mailer.default_url_options = { host: host, protocol: https ? 'https://' : 'http://', trailing_slash: false }
|
||||
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'
|
||||
|
||||
if Rails.env.production?
|
||||
config.action_cable.allowed_request_origins = ["http#{https ? 's' : ''}://#{host}"]
|
||||
config.x.streaming_api_base_url = ENV.fetch('STREAMING_API_BASE_URL') { "http#{https ? 's' : ''}://#{host}" }
|
||||
config.action_cable.allowed_request_origins = ["http#{https ? 's' : ''}://#{web_host}"]
|
||||
config.x.streaming_api_base_url = ENV.fetch('STREAMING_API_BASE_URL') { "http#{https ? 's' : ''}://#{web_host}" }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -38,4 +38,7 @@ if ENV['S3_ENABLED'] == 'true'
|
|||
Paperclip::Attachment.default_options[:url] = ':s3_alias_url'
|
||||
Paperclip::Attachment.default_options[:s3_host_alias] = ENV['S3_CLOUDFRONT_HOST']
|
||||
end
|
||||
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
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
host = ENV.fetch('REDIS_HOST') { 'localhost' }
|
||||
port = ENV.fetch('REDIS_PORT') { 6379 }
|
||||
password = ENV.fetch('REDIS_PASSWORD') { false }
|
||||
db = ENV.fetch('REDIS_DB') { 0 }
|
||||
|
||||
Sidekiq.configure_server do |config|
|
||||
config.redis = { host: host, port: port, password: password}
|
||||
config.redis = { host: host, port: port, db: db, password: password }
|
||||
end
|
||||
|
||||
Sidekiq.configure_client do |config|
|
||||
config.redis = { host: host, port: port, password: password }
|
||||
config.redis = { host: host, port: port, db: db, password: password }
|
||||
end
|
||||
|
|
|
@ -165,5 +165,3 @@ bg:
|
|||
users:
|
||||
invalid_email: E-mail адресът е невалиден
|
||||
invalid_otp_token: Невалиден код
|
||||
will_paginate:
|
||||
page_gap: "…"
|
||||
|
|
|
@ -6,12 +6,12 @@ fr:
|
|||
send_instructions: Vous allez recevoir les instructions nécessaires à la confirmation de votre compte dans quelques minutes.
|
||||
send_paranoid_instructions: Si votre e-mail existe dans notre base de données, vous allez bientôt recevoir un e-mail contenant les instructions de confirmation de votre compte.
|
||||
failure:
|
||||
already_authenticated: Vous êtes déjà connecté
|
||||
inactive: Votre compte n'est pas encore activé.
|
||||
invalid: Email ou mot de passe incorrect.
|
||||
already_authenticated: Vous êtes déjà connecté⋅e
|
||||
inactive: Votre compte n’est pas encore activé.
|
||||
invalid: Courriel ou mot de passe incorrect.
|
||||
last_attempt: Vous avez droit à une tentative avant que votre compte ne soit verrouillé.
|
||||
locked: Votre compte est verrouillé.
|
||||
not_found_in_database: Email ou mot de passe invalide.
|
||||
not_found_in_database: Courriel ou mot de passe invalide.
|
||||
timeout: Votre session a expiré. Veuillez vous reconnecter pour continuer.
|
||||
unauthenticated: Vous devez vous connecter ou vous inscrire pour continuer.
|
||||
unconfirmed: Vous devez valider votre compte pour continuer.
|
||||
|
@ -25,7 +25,7 @@ fr:
|
|||
unlock_instructions:
|
||||
subject: Instructions pour déverrouiller votre compte
|
||||
omniauth_callbacks:
|
||||
failure: 'Nous n''avons pas pu vous authentifier via %{kind} : ''%{reason}''.'
|
||||
failure: 'Nous n’avons pas pu vous authentifier via %{kind} : ''%{reason}''.'
|
||||
success: Authentifié avec succès via %{kind}.
|
||||
passwords:
|
||||
no_token: Vous ne pouvez accéder à cette page sans passer par un e-mail de réinitialisation de mot de passe. Si vous êtes passé⋅e par un e-mail de ce type, assurez-vous d'utiliser l'URL complète.
|
||||
|
@ -36,10 +36,10 @@ fr:
|
|||
registrations:
|
||||
destroyed: Votre compte a été supprimé avec succès. Nous espérons vous revoir bientôt.
|
||||
signed_up: Bienvenue, vous êtes connecté⋅e.
|
||||
signed_up_but_inactive: Vous êtes bien enregistré⋅e. Vous ne pouvez cependant pas vous connecter car votre compte n'est pas encore activé.
|
||||
signed_up_but_inactive: Vous êtes bien enregistré⋅e. Vous ne pouvez cependant pas vous connecter car votre compte n’est pas encore activé.
|
||||
signed_up_but_locked: Vous êtes bien enregistré⋅e. Vous ne pouvez cependant pas vous connecter car votre compte est verrouillé.
|
||||
signed_up_but_unconfirmed: Un message contenant un lien de confirmation a été envoyé à votre adresse email. Ouvrez ce lien pour activer votre compte.
|
||||
update_needs_confirmation: Votre compte a bien été mis à jour mais nous devons vérifier votre nouvelle adresse email. Merci de vérifier vos emails et de cliquer sur le lien de confirmation pour finaliser la validation de votre nouvelle adresse.
|
||||
signed_up_but_unconfirmed: Un message contenant un lien de confirmation a été envoyé à votre adresse courriel. Ouvrez ce lien pour activer votre compte.
|
||||
update_needs_confirmation: Votre compte a bien été mis à jour mais nous devons vérifier votre nouvelle adresse courriel. Merci de vérifier vos courriels et de cliquer sur le lien de confirmation pour finaliser la validation de votre nouvelle adresse.
|
||||
updated: Votre compte a été modifié avec succès.
|
||||
sessions:
|
||||
already_signed_out: Déconnecté.
|
||||
|
@ -47,15 +47,15 @@ fr:
|
|||
signed_out: Déconnecté.
|
||||
unlocks:
|
||||
send_instructions: Vous allez recevoir les instructions nécessaires au déverrouillage de votre compte dans quelques instants
|
||||
send_paranoid_instructions: Si votre compte existe, vous allez bientôt recevoir un email contenant les instructions pour le déverrouiller.
|
||||
send_paranoid_instructions: Si votre compte existe, vous allez bientôt recevoir un courriel contenant les instructions pour le déverrouiller.
|
||||
unlocked: Votre compte a été déverrouillé avec succès, vous êtes maintenant connecté⋅e.
|
||||
errors:
|
||||
messages:
|
||||
already_confirmed: a déjà été validé⋅e, veuillez essayer de vous connecter
|
||||
confirmation_period_expired: à confirmer dans les %{period}, merci de faire une nouvelle demande
|
||||
expired: a expiré, merci d'en faire une nouvelle demande
|
||||
not_found: n'a pas été trouvé⋅e
|
||||
not_locked: n'était pas verrouillé⋅e
|
||||
expired: a expiré, merci d’en faire une nouvelle demande
|
||||
not_found: n’a pas été trouvé⋅e
|
||||
not_locked: n’était pas verrouillé⋅e
|
||||
not_saved:
|
||||
one: '1 erreur a empêché ce(tte) %{resource} d’être sauvegardé⋅e :'
|
||||
other: '%{count} erreurs ont empêché %{resource} d’être sauvegardé⋅e :'
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue