Merge branch 'master' into feature-language-detection
This commit is contained in:
commit
9f8375deaa
|
@ -6,3 +6,6 @@ node_modules
|
||||||
storybook
|
storybook
|
||||||
neo4j
|
neo4j
|
||||||
vendor/bundle
|
vendor/bundle
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Service dependencies
|
# Service dependencies
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=redis
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
# REDIS_DB=0
|
||||||
DB_HOST=db
|
DB_HOST=db
|
||||||
DB_USER=postgres
|
DB_USER=postgres
|
||||||
DB_NAME=postgres
|
DB_NAME=postgres
|
||||||
|
@ -11,6 +12,10 @@ DB_PORT=5432
|
||||||
LOCAL_DOMAIN=example.com
|
LOCAL_DOMAIN=example.com
|
||||||
LOCAL_HTTPS=true
|
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
|
# Application secrets
|
||||||
# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
|
# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
|
||||||
PAPERCLIP_SECRET=
|
PAPERCLIP_SECRET=
|
||||||
|
@ -41,6 +46,10 @@ SMTP_FROM_ADDRESS=notifications@example.com
|
||||||
#SMTP_ENABLE_STARTTLS_AUTO=true
|
#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
|
# Optional asset host for multi-server setups
|
||||||
# CDN_HOST=assets.example.com
|
# CDN_HOST=assets.example.com
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
# Federation
|
# Federation
|
||||||
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||||
LOCAL_HTTPS=true
|
LOCAL_HTTPS=true
|
||||||
|
OTP_SECRET=100c7faeef00caa29242f6b04156742bf76065771fd4117990c4282b8748ff3d99f8fdae97c982ab5bd2e6756a159121377cce4421f4a8ecd2d67bd7749a3fb4
|
||||||
|
|
34
.eslintrc
34
.eslintrc
|
@ -8,7 +8,8 @@
|
||||||
"parser": "babel-eslint",
|
"parser": "babel-eslint",
|
||||||
|
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"react"
|
"react",
|
||||||
|
"jsx-a11y"
|
||||||
],
|
],
|
||||||
|
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
@ -43,9 +44,36 @@
|
||||||
"no-mixed-spaces-and-tabs": 1,
|
"no-mixed-spaces-and-tabs": 1,
|
||||||
"no-nested-ternary": 1,
|
"no-nested-ternary": 1,
|
||||||
"no-trailing-spaces": 1,
|
"no-trailing-spaces": 1,
|
||||||
"react/wrap-multilines": 2,
|
|
||||||
|
"react/jsx-wrap-multilines": 2,
|
||||||
"react/self-closing-comp": 2,
|
"react/self-closing-comp": 2,
|
||||||
"react/prop-types": 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
|
# Ignore Capistrano customizations
|
||||||
config/deploy/*
|
config/deploy/*
|
||||||
|
|
||||||
|
|
||||||
# Ignore IDE files
|
# Ignore IDE files
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
# Ignore postgres + redis volume optionally created by docker-compose
|
# Ignore postgres + redis volume optionally created by docker-compose
|
||||||
postgres
|
postgres
|
||||||
redis
|
redis
|
||||||
|
|
||||||
|
# Ignore Apple files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Ignore vim files
|
||||||
|
*~
|
||||||
|
*.swp
|
||||||
|
|
|
@ -5,8 +5,6 @@ notifications:
|
||||||
email: false
|
email: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
matrix:
|
|
||||||
- TRAVIS_NODE_VERSION="4"
|
|
||||||
global:
|
global:
|
||||||
- LOCAL_DOMAIN=cb6e6126.ngrok.io
|
- LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||||
- LOCAL_HTTPS=true
|
- LOCAL_HTTPS=true
|
||||||
|
@ -28,8 +26,7 @@ before_install:
|
||||||
- sudo apt-get -qq update
|
- sudo apt-get -qq update
|
||||||
- sudo apt-get -qq install g++-4.8
|
- sudo apt-get -qq install g++-4.8
|
||||||
install:
|
install:
|
||||||
- nvm install $TRAVIS_NODE_VERSION
|
- nvm install
|
||||||
- npm install -g npm@3
|
|
||||||
- npm install -g yarn
|
- npm install -g yarn
|
||||||
- bundle install
|
- bundle install
|
||||||
- yarn install
|
- yarn install
|
||||||
|
|
|
@ -28,7 +28,7 @@ RUN BUILD_DEPS=" \
|
||||||
imagemagick \
|
imagemagick \
|
||||||
&& npm install -g npm@3 && npm install -g yarn \
|
&& npm install -g npm@3 && npm install -g yarn \
|
||||||
&& bundle install --deployment --without test development \
|
&& bundle install --deployment --without test development \
|
||||||
&& yarn \
|
&& yarn --ignore-optional \
|
||||||
&& yarn cache clean \
|
&& yarn cache clean \
|
||||||
&& npm -g cache clean \
|
&& npm -g cache clean \
|
||||||
&& apk del $BUILD_DEPS \
|
&& apk del $BUILD_DEPS \
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -44,6 +44,7 @@ gem 'rabl'
|
||||||
gem 'rack-attack'
|
gem 'rack-attack'
|
||||||
gem 'rack-cors', require: 'rack/cors'
|
gem 'rack-cors', require: 'rack/cors'
|
||||||
gem 'rack-timeout'
|
gem 'rack-timeout'
|
||||||
|
gem 'rails-i18n'
|
||||||
gem 'rails-settings-cached'
|
gem 'rails-settings-cached'
|
||||||
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
|
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
|
||||||
gem 'rqrcode'
|
gem 'rqrcode'
|
||||||
|
@ -70,7 +71,9 @@ group :development, :test do
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
|
gem 'capybara'
|
||||||
gem 'faker'
|
gem 'faker'
|
||||||
|
gem 'microformats2'
|
||||||
gem 'rails-controller-testing'
|
gem 'rails-controller-testing'
|
||||||
gem 'rspec-sidekiq'
|
gem 'rspec-sidekiq'
|
||||||
gem 'simplecov', require: false
|
gem 'simplecov', require: false
|
||||||
|
|
19
Gemfile.lock
19
Gemfile.lock
|
@ -99,6 +99,13 @@ GEM
|
||||||
sshkit (~> 1.3)
|
sshkit (~> 1.3)
|
||||||
capistrano-yarn (2.0.2)
|
capistrano-yarn (2.0.2)
|
||||||
capistrano (~> 3.0)
|
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)
|
chunky_png (1.3.8)
|
||||||
climate_control (0.1.0)
|
climate_control (0.1.0)
|
||||||
cocaine (0.5.8)
|
cocaine (0.5.8)
|
||||||
|
@ -233,6 +240,10 @@ GEM
|
||||||
mail (2.6.4)
|
mail (2.6.4)
|
||||||
mime-types (>= 1.16, < 4)
|
mime-types (>= 1.16, < 4)
|
||||||
method_source (0.8.2)
|
method_source (0.8.2)
|
||||||
|
microformats2 (2.1.0)
|
||||||
|
activesupport
|
||||||
|
json
|
||||||
|
nokogiri
|
||||||
mime-types (3.1)
|
mime-types (3.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2016.0521)
|
mime-types-data (3.2016.0521)
|
||||||
|
@ -308,6 +319,9 @@ GEM
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
rails-html-sanitizer (1.0.3)
|
rails-html-sanitizer (1.0.3)
|
||||||
loofah (~> 2.0)
|
loofah (~> 2.0)
|
||||||
|
rails-i18n (5.0.3)
|
||||||
|
i18n (~> 0.7)
|
||||||
|
railties (~> 5.0)
|
||||||
rails-settings-cached (0.6.5)
|
rails-settings-cached (0.6.5)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
rails_12factor (0.0.3)
|
rails_12factor (0.0.3)
|
||||||
|
@ -447,6 +461,8 @@ GEM
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.2)
|
websocket-extensions (0.1.2)
|
||||||
whatlanguage (1.0.6)
|
whatlanguage (1.0.6)
|
||||||
|
xpath (2.0.0)
|
||||||
|
nokogiri (~> 1.3)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
@ -466,6 +482,7 @@ DEPENDENCIES
|
||||||
capistrano-rails
|
capistrano-rails
|
||||||
capistrano-rbenv
|
capistrano-rbenv
|
||||||
capistrano-yarn
|
capistrano-yarn
|
||||||
|
capybara
|
||||||
coffee-rails (~> 4.1.0)
|
coffee-rails (~> 4.1.0)
|
||||||
devise
|
devise
|
||||||
devise-two-factor
|
devise-two-factor
|
||||||
|
@ -490,6 +507,7 @@ DEPENDENCIES
|
||||||
letter_opener_web
|
letter_opener_web
|
||||||
link_header
|
link_header
|
||||||
lograge
|
lograge
|
||||||
|
microformats2
|
||||||
nokogiri
|
nokogiri
|
||||||
oj
|
oj
|
||||||
ostatus2
|
ostatus2
|
||||||
|
@ -507,6 +525,7 @@ DEPENDENCIES
|
||||||
rack-timeout
|
rack-timeout
|
||||||
rails (~> 5.0.2)
|
rails (~> 5.0.2)
|
||||||
rails-controller-testing
|
rails-controller-testing
|
||||||
|
rails-i18n
|
||||||
rails-settings-cached
|
rails-settings-cached
|
||||||
rails_12factor
|
rails_12factor
|
||||||
react-rails
|
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
|
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 -)"
|
eval "$(rbenv init -)"
|
||||||
|
|
||||||
cd /vagrant
|
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' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
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 = {
|
const buttonsStyle = {
|
||||||
|
@ -25,6 +26,7 @@ const Account = React.createClass({
|
||||||
me: React.PropTypes.number.isRequired,
|
me: React.PropTypes.number.isRequired,
|
||||||
onFollow: React.PropTypes.func.isRequired,
|
onFollow: React.PropTypes.func.isRequired,
|
||||||
onBlock: React.PropTypes.func.isRequired,
|
onBlock: React.PropTypes.func.isRequired,
|
||||||
|
onMute: React.PropTypes.func.isRequired,
|
||||||
intl: React.PropTypes.object.isRequired
|
intl: React.PropTypes.object.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -38,6 +40,10 @@ const Account = React.createClass({
|
||||||
this.props.onBlock(this.props.account);
|
this.props.onBlock(this.props.account);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleMute () {
|
||||||
|
this.props.onMute(this.props.account);
|
||||||
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, me, intl } = this.props;
|
const { account, me, intl } = this.props;
|
||||||
|
|
||||||
|
@ -51,11 +57,14 @@ const Account = React.createClass({
|
||||||
const following = account.getIn(['relationship', 'following']);
|
const following = account.getIn(['relationship', 'following']);
|
||||||
const requested = account.getIn(['relationship', 'requested']);
|
const requested = account.getIn(['relationship', 'requested']);
|
||||||
const blocking = account.getIn(['relationship', 'blocking']);
|
const blocking = account.getIn(['relationship', 'blocking']);
|
||||||
|
const muting = account.getIn(['relationship', 'muting']);
|
||||||
|
|
||||||
if (requested) {
|
if (requested) {
|
||||||
buttons = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />
|
buttons = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />
|
||||||
} else if (blocking) {
|
} 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 {
|
} else {
|
||||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
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'>
|
<div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'>
|
||||||
{suggestions.map((suggestion, i) => (
|
{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} />
|
<AutosuggestAccountContainer id={suggestion} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -9,6 +9,7 @@ const Button = React.createClass({
|
||||||
block: React.PropTypes.bool,
|
block: React.PropTypes.bool,
|
||||||
secondary: React.PropTypes.bool,
|
secondary: React.PropTypes.bool,
|
||||||
size: React.PropTypes.number,
|
size: React.PropTypes.number,
|
||||||
|
style: React.PropTypes.object,
|
||||||
children: React.PropTypes.node
|
children: React.PropTypes.node
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,13 @@ const ColumnBackButton = React.createClass({
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
handleClick () {
|
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();
|
else this.context.router.goBack();
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
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} />
|
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31,7 +31,7 @@ const ColumnBackButtonSlim = React.createClass({
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative' }}>
|
<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} />
|
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -46,7 +46,9 @@ const ColumnCollapsable = React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative' }}>
|
<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 }) }}>
|
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
|
||||||
{({ opacity, height }) =>
|
{({ opacity, height }) =>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const LoadMore = ({ onClick }) => (
|
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' />
|
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
@ -220,7 +220,7 @@ const MediaGallery = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
children = (
|
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={spoilerSpanStyle}>{warning}</span>
|
||||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,8 @@ const Permalink = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
href: React.PropTypes.string.isRequired,
|
href: React.PropTypes.string.isRequired,
|
||||||
to: React.PropTypes.string.isRequired
|
to: React.PropTypes.string.isRequired,
|
||||||
|
children: React.PropTypes.node
|
||||||
},
|
},
|
||||||
|
|
||||||
handleClick (e) {
|
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 });
|
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 (
|
return (
|
||||||
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
<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 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={{ 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' }}>
|
<div style={{ width: '18px', height: '18px', float: 'left' }}>
|
||||||
|
|
|
@ -119,7 +119,7 @@ const StatusContent = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
<div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||||
<p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} >
|
<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>
|
</p>
|
||||||
|
|
||||||
{mentionsPlaceholder}
|
{mentionsPlaceholder}
|
||||||
|
|
|
@ -194,7 +194,7 @@ const VideoPlayer = React.createClass({
|
||||||
if (!this.state.visible) {
|
if (!this.state.visible) {
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
return (
|
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}
|
{spoilerButton}
|
||||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
|
<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>
|
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||||
|
@ -202,7 +202,7 @@ const VideoPlayer = React.createClass({
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
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}
|
{spoilerButton}
|
||||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
|
<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>
|
<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) {
|
if (this.state.preview && !autoplay) {
|
||||||
return (
|
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}
|
{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 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>
|
</div>
|
||||||
|
@ -225,7 +225,7 @@ const VideoPlayer = React.createClass({
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
{muteButton}
|
{muteButton}
|
||||||
{expandButton}
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ import FollowRequests from '../features/follow_requests';
|
||||||
import GenericNotFound from '../features/generic_not_found';
|
import GenericNotFound from '../features/generic_not_found';
|
||||||
import FavouritedStatuses from '../features/favourited_statuses';
|
import FavouritedStatuses from '../features/favourited_statuses';
|
||||||
import Blocks from '../features/blocks';
|
import Blocks from '../features/blocks';
|
||||||
|
import Mutes from '../features/mutes';
|
||||||
import Report from '../features/report';
|
import Report from '../features/report';
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
import en from 'react-intl/locale-data/en';
|
import en from 'react-intl/locale-data/en';
|
||||||
|
@ -60,8 +61,8 @@ import { hydrateStore } from '../actions/store';
|
||||||
import createStream from '../stream';
|
import createStream from '../stream';
|
||||||
|
|
||||||
const store = configureStore();
|
const store = configureStore();
|
||||||
|
const initialState = JSON.parse(document.getElementById("initial-state").textContent);
|
||||||
store.dispatch(hydrateStore(window.INITIAL_STATE));
|
store.dispatch(hydrateStore(initialState));
|
||||||
|
|
||||||
const browserHistory = useRouterHistory(createBrowserHistory)({
|
const browserHistory = useRouterHistory(createBrowserHistory)({
|
||||||
basename: '/web'
|
basename: '/web'
|
||||||
|
@ -94,9 +95,10 @@ const Mastodon = React.createClass({
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { locale } = this.props;
|
const { locale } = this.props;
|
||||||
|
const streamingAPIBaseURL = store.getState().getIn(['meta', 'streaming_api_base_url']);
|
||||||
const accessToken = store.getState().getIn(['meta', 'access_token']);
|
const accessToken = store.getState().getIn(['meta', 'access_token']);
|
||||||
|
|
||||||
this.subscription = createStream(accessToken, 'user', {
|
this.subscription = createStream(streamingAPIBaseURL, accessToken, 'user', {
|
||||||
|
|
||||||
connected () {
|
connected () {
|
||||||
store.dispatch(connectTimeline('home'));
|
store.dispatch(connectTimeline('home'));
|
||||||
|
@ -171,6 +173,7 @@ const Mastodon = React.createClass({
|
||||||
|
|
||||||
<Route path='follow_requests' component={FollowRequests} />
|
<Route path='follow_requests' component={FollowRequests} />
|
||||||
<Route path='blocks' component={Blocks} />
|
<Route path='blocks' component={Blocks} />
|
||||||
|
<Route path='mutes' component={Mutes} />
|
||||||
<Route path='report' component={Report} />
|
<Route path='report' component={Report} />
|
||||||
|
|
||||||
<Route path='*' component={GenericNotFound} />
|
<Route path='*' component={GenericNotFound} />
|
||||||
|
|
|
@ -43,7 +43,16 @@ const Avatar = React.createClass({
|
||||||
return (
|
return (
|
||||||
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
|
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
|
||||||
{({ radius }) =>
|
{({ 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' }} />
|
<img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
|
||||||
|
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||||
accessToken: state.getIn(['meta', 'access_token'])
|
accessToken: state.getIn(['meta', 'access_token'])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ const CommunityTimeline = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
dispatch: React.PropTypes.func.isRequired,
|
dispatch: React.PropTypes.func.isRequired,
|
||||||
intl: React.PropTypes.object.isRequired,
|
intl: React.PropTypes.object.isRequired,
|
||||||
|
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||||
accessToken: React.PropTypes.string.isRequired,
|
accessToken: React.PropTypes.string.isRequired,
|
||||||
hasUnread: React.PropTypes.bool
|
hasUnread: React.PropTypes.bool
|
||||||
},
|
},
|
||||||
|
@ -36,7 +38,7 @@ const CommunityTimeline = React.createClass({
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch, accessToken } = this.props;
|
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
|
||||||
|
|
||||||
dispatch(refreshTimeline('community'));
|
dispatch(refreshTimeline('community'));
|
||||||
|
|
||||||
|
@ -44,7 +46,7 @@ const CommunityTimeline = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription = createStream(accessToken, 'public:local', {
|
subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
|
||||||
|
|
||||||
connected () {
|
connected () {
|
||||||
dispatch(connectTimeline('community'));
|
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__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'>
|
<div className='privacy-dropdown__dropdown'>
|
||||||
{options.map(item =>
|
{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__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
|
||||||
<div className='privacy-dropdown__option__content'>
|
<div className='privacy-dropdown__option__content'>
|
||||||
<strong>{item.shortText}</strong>
|
<strong>{item.shortText}</strong>
|
||||||
|
|
|
@ -36,6 +36,10 @@ const Search = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
noop () {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
handleFocus () {
|
handleFocus () {
|
||||||
this.props.onShow();
|
this.props.onShow();
|
||||||
},
|
},
|
||||||
|
@ -56,9 +60,9 @@ const Search = React.createClass({
|
||||||
onFocus={this.handleFocus}
|
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-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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -50,7 +50,7 @@ const Followers = React.createClass({
|
||||||
|
|
||||||
handleLoadMore (e) {
|
handleLoadMore (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
|
this.props.dispatch(expandFollowers(Number(this.props.params.accountId)));
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|
|
@ -14,6 +14,7 @@ const messages = defineMessages({
|
||||||
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||||
|
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||||
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }
|
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' />
|
<ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
|
||||||
{followRequests}
|
{followRequests}
|
||||||
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
<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='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' />
|
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,6 +13,7 @@ import createStream from '../../stream';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
|
||||||
|
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||||
accessToken: state.getIn(['meta', 'access_token'])
|
accessToken: state.getIn(['meta', 'access_token'])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ const HashtagTimeline = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
params: React.PropTypes.object.isRequired,
|
params: React.PropTypes.object.isRequired,
|
||||||
dispatch: React.PropTypes.func.isRequired,
|
dispatch: React.PropTypes.func.isRequired,
|
||||||
|
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||||
accessToken: React.PropTypes.string.isRequired,
|
accessToken: React.PropTypes.string.isRequired,
|
||||||
hasUnread: React.PropTypes.bool
|
hasUnread: React.PropTypes.bool
|
||||||
},
|
},
|
||||||
|
@ -28,9 +30,9 @@ const HashtagTimeline = React.createClass({
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
_subscribe (dispatch, id) {
|
_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) {
|
received (data) {
|
||||||
switch(data.event) {
|
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;
|
const { intl } = this.props;
|
||||||
|
|
||||||
return (
|
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' />
|
<i className='fa fa-eraser' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -27,9 +27,11 @@ const ColumnSettings = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
settings: ImmutablePropTypes.map.isRequired,
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
intl: React.PropTypes.object.isRequired,
|
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: React.PropTypes.func.isRequired,
|
||||||
onSave: React.PropTypes.func.isRequired,
|
onSave: React.PropTypes.func.isRequired,
|
||||||
|
intl: React.PropTypes.shape({
|
||||||
|
formatMessage: React.PropTypes.func.isRequired
|
||||||
|
}).isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
|
@ -71,7 +71,7 @@ const Notification = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () { // eslint-disable-line consistent-return
|
||||||
const { notification } = this.props;
|
const { notification } = this.props;
|
||||||
const account = notification.get('account');
|
const account = notification.get('account');
|
||||||
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
|
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
|
||||||
|
|
|
@ -14,8 +14,8 @@ const labelSpanStyle = {
|
||||||
marginLeft: '8px'
|
marginLeft: '8px'
|
||||||
};
|
};
|
||||||
|
|
||||||
const SettingToggle = ({ settings, settingKey, label, onChange }) => (
|
const SettingToggle = ({ settings, settingKey, label, onChange, htmlFor = '' }) => (
|
||||||
<label style={labelStyle}>
|
<label htmlFor={htmlFor} style={labelStyle}>
|
||||||
<Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
|
<Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
|
||||||
<span className='setting-toggle' style={labelSpanStyle}>{label}</span>
|
<span className='setting-toggle' style={labelSpanStyle}>{label}</span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -25,7 +25,8 @@ SettingToggle.propTypes = {
|
||||||
settings: ImmutablePropTypes.map.isRequired,
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
settingKey: React.PropTypes.array.isRequired,
|
settingKey: React.PropTypes.array.isRequired,
|
||||||
label: React.PropTypes.node.isRequired,
|
label: React.PropTypes.node.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired
|
onChange: React.PropTypes.func.isRequired,
|
||||||
|
htmlFor: React.PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingToggle;
|
export default SettingToggle;
|
||||||
|
|
|
@ -19,6 +19,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
|
||||||
|
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||||
accessToken: state.getIn(['meta', 'access_token'])
|
accessToken: state.getIn(['meta', 'access_token'])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ const PublicTimeline = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
dispatch: React.PropTypes.func.isRequired,
|
dispatch: React.PropTypes.func.isRequired,
|
||||||
intl: React.PropTypes.object.isRequired,
|
intl: React.PropTypes.object.isRequired,
|
||||||
|
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||||
accessToken: React.PropTypes.string.isRequired,
|
accessToken: React.PropTypes.string.isRequired,
|
||||||
hasUnread: React.PropTypes.bool
|
hasUnread: React.PropTypes.bool
|
||||||
},
|
},
|
||||||
|
@ -36,7 +38,7 @@ const PublicTimeline = React.createClass({
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch, accessToken } = this.props;
|
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
|
||||||
|
|
||||||
dispatch(refreshTimeline('public'));
|
dispatch(refreshTimeline('public'));
|
||||||
|
|
||||||
|
@ -44,7 +46,7 @@ const PublicTimeline = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription = createStream(accessToken, 'public', {
|
subscription = createStream(streamingAPIBaseURL, accessToken, 'public', {
|
||||||
|
|
||||||
connected () {
|
connected () {
|
||||||
dispatch(connectTimeline('public'));
|
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 });
|
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 (
|
return (
|
||||||
<div className='detailed-status__action-bar'>
|
<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 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' }}><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 style={{ flex: '1 1 auto', textAlign: 'center' }}><DropdownMenu size={18} icon='ellipsis-h' items={menu} direction="left" /></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,7 +25,7 @@ const ColumnHeader = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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}
|
{icon}
|
||||||
{type}
|
{type}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -34,7 +34,8 @@ ColumnLink.propTypes = {
|
||||||
icon: React.PropTypes.string.isRequired,
|
icon: React.PropTypes.string.isRequired,
|
||||||
text: React.PropTypes.string.isRequired,
|
text: React.PropTypes.string.isRequired,
|
||||||
to: React.PropTypes.string,
|
to: React.PropTypes.string,
|
||||||
href: React.PropTypes.string
|
href: React.PropTypes.string,
|
||||||
|
method: React.PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ColumnLink;
|
export default ColumnLink;
|
||||||
|
|
|
@ -104,8 +104,8 @@ const MediaModal = React.createClass({
|
||||||
leftNav = rightNav = content = '';
|
leftNav = rightNav = content = '';
|
||||||
|
|
||||||
if (media.size > 1) {
|
if (media.size > 1) {
|
||||||
leftNav = <div style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></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 style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></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') {
|
if (attachment.get('type') === 'image') {
|
||||||
|
|
|
@ -66,7 +66,7 @@ const ModalRoot = React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key}>
|
<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})` }}>
|
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
|
||||||
<SpecificComponent {...props} onClose={onClose} />
|
<SpecificComponent {...props} onClose={onClose} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -141,7 +141,7 @@ const UI = React.createClass({
|
||||||
{mountedColumns}
|
{mountedColumns}
|
||||||
|
|
||||||
<NotificationsContainer />
|
<NotificationsContainer />
|
||||||
<LoadingBarContainer style={{ backgroundColor: '#2b90d9', left: '0', top: '0' }} />
|
<LoadingBarContainer className="loading-bar" />
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<UploadArea active={draggingOver} />
|
<UploadArea active={draggingOver} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,7 +14,7 @@ Link.parseAttrs = (link, parts) => {
|
||||||
link = Link.parseParams(link, uriAttrs[1])
|
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()
|
attr = match[1].toLowerCase()
|
||||||
value = match[4] || match[3] || match[2]
|
value = match[4] || match[3] || match[2]
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ const en = {
|
||||||
"column.favourites": "Favourites",
|
"column.favourites": "Favourites",
|
||||||
"column.follow_requests": "Follow requests",
|
"column.follow_requests": "Follow requests",
|
||||||
"column.home": "Home",
|
"column.home": "Home",
|
||||||
|
"column.mutes": "Muted users",
|
||||||
"column.notifications": "Notifications",
|
"column.notifications": "Notifications",
|
||||||
"column.public": "Federated timeline",
|
"column.public": "Federated timeline",
|
||||||
"compose_form.placeholder": "What is on your mind?",
|
"compose_form.placeholder": "What is on your mind?",
|
||||||
|
@ -68,6 +69,7 @@ const en = {
|
||||||
"navigation_bar.follow_requests": "Follow requests",
|
"navigation_bar.follow_requests": "Follow requests",
|
||||||
"navigation_bar.info": "Extended information",
|
"navigation_bar.info": "Extended information",
|
||||||
"navigation_bar.logout": "Logout",
|
"navigation_bar.logout": "Logout",
|
||||||
|
"navigation_bar.mutes": "Muted users",
|
||||||
"navigation_bar.preferences": "Preferences",
|
"navigation_bar.preferences": "Preferences",
|
||||||
"navigation_bar.public_timeline": "Federated timeline",
|
"navigation_bar.public_timeline": "Federated timeline",
|
||||||
"notification.favourite": "{name} favourited your status",
|
"notification.favourite": "{name} favourited your status",
|
||||||
|
|
|
@ -37,6 +37,7 @@ const ja = {
|
||||||
"getting_started.about_addressing": "ドメインとユーザー名を知っているなら検索フォームに入力すればフォローできます。",
|
"getting_started.about_addressing": "ドメインとユーザー名を知っているなら検索フォームに入力すればフォローできます。",
|
||||||
"getting_started.about_shortcuts": "対象のアカウントがあなたと同じドメインのユーザーならばユーザー名のみで検索できます。これは返信のときも一緒です。",
|
"getting_started.about_shortcuts": "対象のアカウントがあなたと同じドメインのユーザーならばユーザー名のみで検索できます。これは返信のときも一緒です。",
|
||||||
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub({github})から開発に参加したり、問題を報告したりできます。 {apps}",
|
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub({github})から開発に参加したり、問題を報告したりできます。 {apps}",
|
||||||
|
"getting_started.apps": "さまざまなアプリで利用できます。",
|
||||||
"column.home": "ホーム",
|
"column.home": "ホーム",
|
||||||
"column.community": "ローカルタイムライン",
|
"column.community": "ローカルタイムライン",
|
||||||
"column.public": "連合タイムライン",
|
"column.public": "連合タイムライン",
|
||||||
|
@ -64,7 +65,7 @@ const ja = {
|
||||||
"privacy.private.long": "フォロワーだけに公開",
|
"privacy.private.long": "フォロワーだけに公開",
|
||||||
"privacy.direct.short": "ダイレクト",
|
"privacy.direct.short": "ダイレクト",
|
||||||
"privacy.direct.long": "含んだユーザーだけに公開",
|
"privacy.direct.long": "含んだユーザーだけに公開",
|
||||||
"privacy.change": "投稿のプライバシーを変更2",
|
"privacy.change": "投稿のプライバシーを変更",
|
||||||
"report.heading": "新規通報",
|
"report.heading": "新規通報",
|
||||||
"report.placeholder": "コメント",
|
"report.placeholder": "コメント",
|
||||||
"report.target": "問題のユーザー",
|
"report.target": "問題のユーザー",
|
||||||
|
@ -82,6 +83,7 @@ const ja = {
|
||||||
"search.account": "アカウント",
|
"search.account": "アカウント",
|
||||||
"search.hashtag": "ハッシュタグ",
|
"search.hashtag": "ハッシュタグ",
|
||||||
"search.status_by": "{uuuname}からの投稿",
|
"search.status_by": "{uuuname}からの投稿",
|
||||||
|
"search_results.total": "{count} 件",
|
||||||
"upload_area.title": "ファイルをこちらにドラッグしてください",
|
"upload_area.title": "ファイルをこちらにドラッグしてください",
|
||||||
"upload_button.label": "メディアを追加",
|
"upload_button.label": "メディアを追加",
|
||||||
"upload_form.undo": "やり直す",
|
"upload_form.undo": "やり直す",
|
||||||
|
@ -111,7 +113,7 @@ const ja = {
|
||||||
"home.column_settings.show_replies": "返信表示",
|
"home.column_settings.show_replies": "返信表示",
|
||||||
"home.column_settings.filter_regex": "正規表現でフィルター",
|
"home.column_settings.filter_regex": "正規表現でフィルター",
|
||||||
"home.settings": "カラム設定",
|
"home.settings": "カラム設定",
|
||||||
"notification.settings": "カラム設定",
|
"notifications.settings": "カラム設定",
|
||||||
"missing_indicator.label": "見つかりません",
|
"missing_indicator.label": "見つかりません",
|
||||||
"boost_modal.combo": "次は{combo}を押せば、これをスキップできます。"
|
"boost_modal.combo": "次は{combo}を押せば、これをスキップできます。"
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
const nl = {
|
const nl = {
|
||||||
"column_back_button.label": "terug",
|
"column_back_button.label": "terug",
|
||||||
"lightbox.close": "Sluiten",
|
"lightbox.close": "Sluiten",
|
||||||
"loading_indicator.label": "Laden...",
|
"loading_indicator.label": "Laden…",
|
||||||
"status.mention": "Vermeld @{name}",
|
"status.mention": "@{name} vermelden",
|
||||||
"status.delete": "Verwijder",
|
"status.delete": "Verwijderen",
|
||||||
"status.reply": "Reageer",
|
"status.reply": "Reageren",
|
||||||
"status.reblog": "Boost",
|
"status.reblog": "Boost",
|
||||||
"status.favourite": "Favoriet",
|
"status.favourite": "Favoriet",
|
||||||
"status.reblogged_by": "{name} boostte",
|
"status.reblogged_by": "{name} boostte",
|
||||||
"status.sensitive_warning": "Gevoelige inhoud",
|
"status.sensitive_warning": "Gevoelige inhoud",
|
||||||
"status.sensitive_toggle": "Klik om te zien",
|
"status.sensitive_toggle": "Klik om te zien",
|
||||||
"video_player.toggle_sound": "Geluid omschakelen",
|
"video_player.toggle_sound": "Geluid in-/uitschakelen",
|
||||||
"account.mention": "Vermeld @{name}",
|
"account.mention": "@{name} vermelden",
|
||||||
"account.edit_profile": "Bewerk profiel",
|
"account.edit_profile": "Profiel bewerken",
|
||||||
"account.unblock": "Deblokkeer @{name}",
|
"account.unblock": "@{name} deblokkeren",
|
||||||
"account.unfollow": "Ontvolg",
|
"account.unfollow": "Ontvolgen",
|
||||||
"account.block": "Blokkeer @{name}",
|
"account.block": "@{name} blokkeren",
|
||||||
"account.follow": "Volg",
|
"account.follow": "Volgen",
|
||||||
"account.posts": "Berichten",
|
"account.posts": "Berichten",
|
||||||
"account.follows": "Volgt",
|
"account.follows": "Volgt",
|
||||||
"account.followers": "Volgers",
|
"account.followers": "Volgers",
|
||||||
|
@ -25,7 +25,7 @@ const nl = {
|
||||||
"getting_started.heading": "Beginnen",
|
"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_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.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.home": "Thuis",
|
||||||
"column.community": "Lokale tijdlijn",
|
"column.community": "Lokale tijdlijn",
|
||||||
"column.public": "Federatietijdlijn",
|
"column.public": "Federatietijdlijn",
|
||||||
|
@ -37,30 +37,30 @@ const nl = {
|
||||||
"tabs_bar.notifications": "Meldingen",
|
"tabs_bar.notifications": "Meldingen",
|
||||||
"compose_form.placeholder": "Waar ben je mee bezig?",
|
"compose_form.placeholder": "Waar ben je mee bezig?",
|
||||||
"compose_form.publish": "Toot",
|
"compose_form.publish": "Toot",
|
||||||
"compose_form.sensitive": "Markeer media als gevoelig",
|
"compose_form.sensitive": "Media als gevoelig markeren",
|
||||||
"compose_form.spoiler": "Verberg tekst achter waarschuwing",
|
"compose_form.spoiler": "Tekst achter waarschuwing verbergen",
|
||||||
"compose_form.private": "Mark als privé",
|
"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.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",
|
"compose_form.unlisted": "Niet op openbare tijdlijnen tonen",
|
||||||
"navigation_bar.edit_profile": "Bewerk profiel",
|
"navigation_bar.edit_profile": "Profiel bewerken",
|
||||||
"navigation_bar.preferences": "Voorkeuren",
|
"navigation_bar.preferences": "Voorkeuren",
|
||||||
"navigation_bar.community_timeline": "Lokale tijdlijn",
|
"navigation_bar.community_timeline": "Lokale tijdlijn",
|
||||||
"navigation_bar.public_timeline": "Federatietijdlijn",
|
"navigation_bar.public_timeline": "Federatietijdlijn",
|
||||||
"navigation_bar.logout": "Uitloggen",
|
"navigation_bar.logout": "Afmelden",
|
||||||
"reply_indicator.cancel": "Annuleren",
|
"reply_indicator.cancel": "Annuleren",
|
||||||
"search.placeholder": "Zoeken",
|
"search.placeholder": "Zoeken",
|
||||||
"search.account": "Account",
|
"search.account": "Account",
|
||||||
"search.hashtag": "Hashtag",
|
"search.hashtag": "Hashtag",
|
||||||
"upload_button.label": "Toevoegen media",
|
"upload_button.label": "Media toevoegen",
|
||||||
"upload_form.undo": "Ongedaan maken",
|
"upload_form.undo": "Ongedaan maken",
|
||||||
"notification.follow": "{name} volgde jou",
|
"notification.follow": "{name} volgde jou",
|
||||||
"notification.favourite": "{name} markeerde je status als favoriet",
|
"notification.favourite": "{name} markeerde je status als favoriet",
|
||||||
"notification.reblog": "{name} boostte je status",
|
"notification.reblog": "{name} boostte je status",
|
||||||
"notification.mention": "{name} vermeldde jou",
|
"notification.mention": "{name} vermeldde jou",
|
||||||
"notifications.column_settings.alert": "Desktopmeldingen",
|
"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.follow": "Nieuwe volgers:",
|
||||||
"notifications.column_settings.favourite": "Favoriten:",
|
"notifications.column_settings.favourite": "Favorieten:",
|
||||||
"notifications.column_settings.mention": "Vermeldingen:",
|
"notifications.column_settings.mention": "Vermeldingen:",
|
||||||
"notifications.column_settings.reblog": "Boosts:",
|
"notifications.column_settings.reblog": "Boosts:",
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,77 +1,130 @@
|
||||||
const no = {
|
const no = {
|
||||||
"column_back_button.label": "Tilbake",
|
"account.block": "Blokkér @{name}",
|
||||||
"lightbox.close": "Lukk",
|
"account.disclaimer": "Denne brukeren er fra en annen instans. Dette tallet kan være høyere.",
|
||||||
"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.edit_profile": "Rediger profil",
|
"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.unblock": "Avblokker @{name}",
|
||||||
"account.unfollow": "Avfølg",
|
"account.unfollow": "Avfølg",
|
||||||
"account.block": "Blokker @{name}",
|
"account.unmute": "Avdemp @{name}",
|
||||||
"account.follow": "Følg",
|
"boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
|
||||||
"account.posts": "Poster",
|
"column_back_button.label": "Tilbake",
|
||||||
"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",
|
|
||||||
"column.blocks": "Blokkerte brukere",
|
"column.blocks": "Blokkerte brukere",
|
||||||
|
"column.community": "Lokal tidslinje",
|
||||||
"column.favourites": "Likt",
|
"column.favourites": "Likt",
|
||||||
"tabs_bar.compose": "Komponer",
|
"column.follow_requests": "Følgeforespørsler",
|
||||||
"tabs_bar.home": "Hjem",
|
"column.home": "Hjem",
|
||||||
"tabs_bar.mentions": "Nevninger",
|
"column.notifications": "Varslinger",
|
||||||
"tabs_bar.public": "Forent tidslinje",
|
"column.public": "Felles tidslinje",
|
||||||
"tabs_bar.notifications": "Varslinger",
|
|
||||||
"compose_form.placeholder": "Hva har du på hjertet?",
|
"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.publish": "Tut",
|
||||||
"compose_form.sensitive": "Merk media som følsomt",
|
"compose_form.sensitive": "Merk media som følsomt",
|
||||||
|
"compose_form.spoiler_placeholder": "Innholdsadvarsel",
|
||||||
"compose_form.spoiler": "Skjul tekst bak advarsel",
|
"compose_form.spoiler": "Skjul tekst bak advarsel",
|
||||||
"compose_form.private": "Merk som privat",
|
"emoji_button.label": "Sett inn emoji",
|
||||||
"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.",
|
"empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!",
|
||||||
"compose_form.unlisted": "Ikke vis på offentlige tidslinjer",
|
"empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.",
|
||||||
"navigation_bar.edit_profile": "Rediger profil",
|
"empty_column.home.public_timeline": "en offentlig tidslinje",
|
||||||
"navigation_bar.preferences": "Preferanser",
|
"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.",
|
||||||
"navigation_bar.community_timeline": "Lokal tidslinje",
|
"empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.",
|
||||||
"navigation_bar.public_timeline": "Forent tidslinje",
|
"empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp",
|
||||||
"navigation_bar.logout": "Logg ut",
|
"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.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.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",
|
"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.placeholder": "Søk",
|
||||||
"search.account": "Konto",
|
"search.status_by": "Status fra {name}",
|
||||||
"search.hashtag": "Hashtag",
|
"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_button.label": "Legg til media",
|
||||||
"upload_form.undo": "Angre",
|
"upload_form.undo": "Angre",
|
||||||
"notification.follow": "{name} fulgte deg",
|
"upload_progress.label": "Laster opp...",
|
||||||
"notification.favourite": "{name} likte din status",
|
"video_player.toggle_sound": "Veksle lyd",
|
||||||
"notification.reblog": "{name} reblogget din status",
|
"video_player.toggle_visible": "Veksle synlighet",
|
||||||
"notification.mention": "{name} nevnte deg",
|
"video_player.expand": "Utvid video",
|
||||||
"notifications.column_settings.alert": "Skrivebordsvarslinger",
|
"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.",
|
||||||
"notifications.column_settings.show": "Vis i kolonne",
|
"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.",
|
||||||
"notifications.column_settings.follow": "Nye følgere:",
|
"tabs_bar.mentions": "Nevninger",
|
||||||
"notifications.column_settings.favourite": "Likt:",
|
"tabs_bar.public": "Felles tidslinje",
|
||||||
"notifications.column_settings.mention": "Nevninger:",
|
"compose_form.private": "Merk som privat",
|
||||||
"notifications.column_settings.reblog": "Reblogginger:",
|
"compose_form.unlisted": "Ikke vis på offentlige tidslinjer",
|
||||||
|
"search.account": "Konto",
|
||||||
|
"search.hashtag": "Hashtag",
|
||||||
|
"notification.mention": "{name} nevnte deg"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default no;
|
export default no;
|
||||||
|
|
|
@ -2,7 +2,9 @@ const ru = {
|
||||||
"column_back_button.label": "Назад",
|
"column_back_button.label": "Назад",
|
||||||
"lightbox.close": "Закрыть",
|
"lightbox.close": "Закрыть",
|
||||||
"loading_indicator.label": "Загрузка...",
|
"loading_indicator.label": "Загрузка...",
|
||||||
|
"missing_indicator.label": "Не найдено",
|
||||||
"status.mention": "Упомянуть @{name}",
|
"status.mention": "Упомянуть @{name}",
|
||||||
|
"status.media_hidden": "Медиаконтент скрыт",
|
||||||
"status.delete": "Удалить",
|
"status.delete": "Удалить",
|
||||||
"status.reply": "Ответить",
|
"status.reply": "Ответить",
|
||||||
"status.reblog": "Продвинуть",
|
"status.reblog": "Продвинуть",
|
||||||
|
@ -16,18 +18,23 @@ const ru = {
|
||||||
"status.report": "Пожаловаться",
|
"status.report": "Пожаловаться",
|
||||||
"status.load_more": "Показать еще",
|
"status.load_more": "Показать еще",
|
||||||
"video_player.toggle_sound": "Вкл./выкл. звук",
|
"video_player.toggle_sound": "Вкл./выкл. звук",
|
||||||
|
"video_player.toggle_visible": "Показать/скрыть",
|
||||||
|
"account.disclaimer": "Это пользователь с другого узла. Число может быть больше.",
|
||||||
"account.mention": "Упомянуть",
|
"account.mention": "Упомянуть",
|
||||||
"account.edit_profile": "Изменить профиль",
|
"account.edit_profile": "Изменить профиль",
|
||||||
"account.unblock": "Разблокировать",
|
"account.unblock": "Разблокировать",
|
||||||
"account.unfollow": "Отписаться",
|
"account.unfollow": "Отписаться",
|
||||||
"account.block": "Блокировать",
|
"account.block": "Блокировать",
|
||||||
"account.mute": "Заглушить",
|
"account.mute": "Заглушить",
|
||||||
|
"account.report": "Пожаловаться",
|
||||||
|
"account.unmute": "Снять глушение",
|
||||||
"account.follow": "Подписаться",
|
"account.follow": "Подписаться",
|
||||||
"account.posts": "Посты",
|
"account.posts": "Посты",
|
||||||
"account.follows": "Подписки",
|
"account.follows": "Подписки",
|
||||||
"account.followers": "Подписаны",
|
"account.followers": "Подписаны",
|
||||||
"account.follows_you": "Подписан(а) на Вас",
|
"account.follows_you": "Подписан(а) на Вас",
|
||||||
"account.requested": "Ожидает подтверждения",
|
"account.requested": "Ожидает подтверждения",
|
||||||
|
"boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
|
||||||
"getting_started.heading": "Добро пожаловать",
|
"getting_started.heading": "Добро пожаловать",
|
||||||
"getting_started.about_addressing": "Вы можете подписаться на человека, зная имя пользователя и домен, на котором он находится, введя e-mail-подобный адрес в форму поиска.",
|
"getting_started.about_addressing": "Вы можете подписаться на человека, зная имя пользователя и домен, на котором он находится, введя e-mail-подобный адрес в форму поиска.",
|
||||||
"getting_started.about_shortcuts": "Если пользователь находится на одном с Вами домене, можно использовать только имя. То же правило применимо к упоминанию пользователей в статусах.",
|
"getting_started.about_shortcuts": "Если пользователь находится на одном с Вами домене, можно использовать только имя. То же правило применимо к упоминанию пользователей в статусах.",
|
||||||
|
@ -37,11 +44,16 @@ const ru = {
|
||||||
"column.community": "Локальная лента",
|
"column.community": "Локальная лента",
|
||||||
"column.public": "Глобальная лента",
|
"column.public": "Глобальная лента",
|
||||||
"column.notifications": "Уведомления",
|
"column.notifications": "Уведомления",
|
||||||
|
"column.favourites": "Понравившееся",
|
||||||
|
"column.blocks": "Список блокировки",
|
||||||
|
"column.follow_requests": "Запросы на подписку",
|
||||||
"tabs_bar.compose": "Написать",
|
"tabs_bar.compose": "Написать",
|
||||||
"tabs_bar.home": "Главная",
|
"tabs_bar.home": "Главная",
|
||||||
"tabs_bar.mentions": "Упоминания",
|
"tabs_bar.mentions": "Упоминания",
|
||||||
"tabs_bar.public": "Глобальная лента",
|
"tabs_bar.public": "Глобальная лента",
|
||||||
"tabs_bar.notifications": "Уведомления",
|
"tabs_bar.notifications": "Уведомления",
|
||||||
|
"tabs_bar.local_timeline": "Локальная",
|
||||||
|
"tabs_bar.federated_timeline": "Глобальная",
|
||||||
"compose_form.placeholder": "О чем Вы думаете?",
|
"compose_form.placeholder": "О чем Вы думаете?",
|
||||||
"compose_form.publish": "Трубить",
|
"compose_form.publish": "Трубить",
|
||||||
"compose_form.sensitive": "Отметить как чувствительный контент",
|
"compose_form.sensitive": "Отметить как чувствительный контент",
|
||||||
|
@ -49,6 +61,7 @@ const ru = {
|
||||||
"compose_form.private": "Отметить как приватное",
|
"compose_form.private": "Отметить как приватное",
|
||||||
"compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.",
|
"compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.",
|
||||||
"compose_form.unlisted": "Не отображать в публичных лентах",
|
"compose_form.unlisted": "Не отображать в публичных лентах",
|
||||||
|
"compose_form.spoiler_placeholder": "Не для всех",
|
||||||
"navigation_bar.edit_profile": "Изменить профиль",
|
"navigation_bar.edit_profile": "Изменить профиль",
|
||||||
"navigation_bar.preferences": "Опции",
|
"navigation_bar.preferences": "Опции",
|
||||||
"navigation_bar.community_timeline": "Локальная лента",
|
"navigation_bar.community_timeline": "Локальная лента",
|
||||||
|
@ -57,12 +70,20 @@ const ru = {
|
||||||
"navigation_bar.info": "Об узле",
|
"navigation_bar.info": "Об узле",
|
||||||
"navigation_bar.favourites": "Понравившееся",
|
"navigation_bar.favourites": "Понравившееся",
|
||||||
"navigation_bar.blocks": "Список блокировки",
|
"navigation_bar.blocks": "Список блокировки",
|
||||||
|
"navigation_bar.follow_requests": "Запросы на подписку",
|
||||||
"reply_indicator.cancel": "Отмена",
|
"reply_indicator.cancel": "Отмена",
|
||||||
|
"report.target": "Жалуемся на",
|
||||||
|
"report.heading": "Новая жалоба",
|
||||||
|
"report.placeholder": "Комментарий",
|
||||||
|
"report.submit": "Отправить",
|
||||||
"search.placeholder": "Поиск",
|
"search.placeholder": "Поиск",
|
||||||
"search.account": "Аккаунт",
|
"search.account": "Аккаунт",
|
||||||
"search.hashtag": "Хэштег",
|
"search.hashtag": "Хэштег",
|
||||||
|
"search.status_by": "Статус от {name}",
|
||||||
|
"upload_area.title": "Перетащите сюда, чтобы загрузить",
|
||||||
"upload_button.label": "Добавить медиаконтент",
|
"upload_button.label": "Добавить медиаконтент",
|
||||||
"upload_form.undo": "Отменить",
|
"upload_form.undo": "Отменить",
|
||||||
|
"upload_progress.label": "Загрузка...",
|
||||||
"notification.follow": "{name} подписался(-лась) на Вас",
|
"notification.follow": "{name} подписался(-лась) на Вас",
|
||||||
"notification.favourite": "{name} понравился Ваш статус",
|
"notification.favourite": "{name} понравился Ваш статус",
|
||||||
"notification.reblog": "{name} продвинул(а) Ваш статус",
|
"notification.reblog": "{name} продвинул(а) Ваш статус",
|
||||||
|
@ -71,9 +92,10 @@ const ru = {
|
||||||
"home.column_settings.basic": "Основные",
|
"home.column_settings.basic": "Основные",
|
||||||
"home.column_settings.advanced": "Дополнительные",
|
"home.column_settings.advanced": "Дополнительные",
|
||||||
"home.column_settings.filter_regex": "Отфильтровать регулярным выражением",
|
"home.column_settings.filter_regex": "Отфильтровать регулярным выражением",
|
||||||
"home.column_settings.show_replies": "Показывать продвижения",
|
"home.column_settings.show_reblogs": "Показывать продвижения",
|
||||||
"home.column_settings.show_replies": "Показывать ответы",
|
"home.column_settings.show_replies": "Показывать ответы",
|
||||||
"notifications.clear": "Очистить уведомления",
|
"notifications.clear": "Очистить уведомления",
|
||||||
|
"notifications.clear_confirmation": "Вы уверены, что хотите очистить все уведомления?",
|
||||||
"notifications.settings": "Настройки колонки",
|
"notifications.settings": "Настройки колонки",
|
||||||
"notifications.column_settings.alert": "Десктопные уведомления",
|
"notifications.column_settings.alert": "Десктопные уведомления",
|
||||||
"notifications.column_settings.show": "Показывать в колонке",
|
"notifications.column_settings.show": "Показывать в колонке",
|
||||||
|
@ -96,6 +118,10 @@ const ru = {
|
||||||
"privacy.private.long": "Показать только подписчикам",
|
"privacy.private.long": "Показать только подписчикам",
|
||||||
"privacy.direct.short": "Направленный",
|
"privacy.direct.short": "Направленный",
|
||||||
"privacy.direct.long": "Показать только упомянутым",
|
"privacy.direct.long": "Показать только упомянутым",
|
||||||
|
"emoji_button.label": "Вставить эмодзи",
|
||||||
|
"follow_request.authorize": "Авторизовать",
|
||||||
|
"follow_request.reject": "Отказать",
|
||||||
|
"media_gallery.toggle_visible": "Показать/скрыть",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ru;
|
export default ru;
|
||||||
|
|
|
@ -19,19 +19,27 @@ export { localeData as localeData };
|
||||||
|
|
||||||
const zh_hk = {
|
const zh_hk = {
|
||||||
"account.block": "封鎖 @{name}",
|
"account.block": "封鎖 @{name}",
|
||||||
|
"account.disclaimer": "由於這個用戶在另一個服務站,實際數字會比這個更多。",
|
||||||
"account.edit_profile": "修改個人資料",
|
"account.edit_profile": "修改個人資料",
|
||||||
"account.follow": "關注",
|
"account.follow": "關注",
|
||||||
"account.followers": "關注的人",
|
"account.followers": "關注的人",
|
||||||
"account.follows_you": "關注你",
|
"account.follows_you": "關注你",
|
||||||
"account.follows": "正在關注",
|
"account.follows": "正在關注",
|
||||||
"account.mention": "提及 @{name}",
|
"account.mention": "提及 @{name}",
|
||||||
|
"account.mute": "將 @{name} 靜音",
|
||||||
"account.posts": "文章",
|
"account.posts": "文章",
|
||||||
|
"account.report": "舉報 @{name}",
|
||||||
"account.requested": "等候審批",
|
"account.requested": "等候審批",
|
||||||
"account.unblock": "解除對 @{name} 的封鎖",
|
"account.unblock": "解除對 @{name} 的封鎖",
|
||||||
"account.unfollow": "取消關注",
|
"account.unfollow": "取消關注",
|
||||||
"column_back_button.label": "先前顯示",
|
"account.unmute": "取消 @{name} 的靜音",
|
||||||
|
"boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
|
||||||
|
"column_back_button.label": "返回",
|
||||||
|
"column.blocks": "封鎖用戶",
|
||||||
"column.community": "本站時間軸",
|
"column.community": "本站時間軸",
|
||||||
"column.home": "家",
|
"column.favourites": "喜歡的文章",
|
||||||
|
"column.follow_requests": "關注請求",
|
||||||
|
"column.home": "主頁",
|
||||||
"column.notifications": "通知",
|
"column.notifications": "通知",
|
||||||
"column.public": "跨站公共時間軸",
|
"column.public": "跨站公共時間軸",
|
||||||
"compose_form.placeholder": "你在想甚麼?",
|
"compose_form.placeholder": "你在想甚麼?",
|
||||||
|
@ -39,35 +47,49 @@ const zh_hk = {
|
||||||
"compose_form.private": "標示為「只有關注你的人能看」",
|
"compose_form.private": "標示為「只有關注你的人能看」",
|
||||||
"compose_form.publish": "發文",
|
"compose_form.publish": "發文",
|
||||||
"compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
|
"compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
|
||||||
|
"compose_form.spoiler_placeholder": "敏感內容",
|
||||||
"compose_form.spoiler": "將部份文字藏於警告訊息之後",
|
"compose_form.spoiler": "將部份文字藏於警告訊息之後",
|
||||||
"compose_form.unlisted": "請勿在公共時間軸顯示",
|
"compose_form.unlisted": "請勿在公共時間軸顯示",
|
||||||
|
"emoji_button.label": "加入表情符號",
|
||||||
"empty_column.community": "本站時間軸暫時未有內容,快貼文來搶頭香啊!",
|
"empty_column.community": "本站時間軸暫時未有內容,快貼文來搶頭香啊!",
|
||||||
"empty_column.hashtag": "這個標籤暫時未有內容。",
|
"empty_column.hashtag": "這個標籤暫時未有內容。",
|
||||||
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
|
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
|
||||||
"empty_column.home.public_timeline": "公共時間軸",
|
"empty_column.home.public_timeline": "公共時間軸",
|
||||||
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
|
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
|
||||||
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up.",
|
"empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。",
|
||||||
|
"empty_column.public": "跨站公共時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。",
|
||||||
|
"follow_request.authorize": "批准",
|
||||||
|
"follow_request.reject": "拒絕",
|
||||||
"getting_started.about_addressing": "只要你知道一位用戶的用戶名稱和域名,你可以用「@用戶名稱@域名」的格式在搜尋欄尋找該用戶。",
|
"getting_started.about_addressing": "只要你知道一位用戶的用戶名稱和域名,你可以用「@用戶名稱@域名」的格式在搜尋欄尋找該用戶。",
|
||||||
"getting_started.about_shortcuts": "只要該用戶是在你現在的服務站開立,你可以直接輸入用戶𠱷搜尋。同樣的規則適用於在文章提及別的用戶。",
|
"getting_started.about_shortcuts": "只要該用戶是在你現在的服務站開立,你可以直接輸入用戶𠱷搜尋。同樣的規則適用於在文章提及別的用戶。",
|
||||||
"getting_started.apps": "手機或桌面應用程式",
|
"getting_started.apps": "手機或桌面應用程式",
|
||||||
"getting_started.heading": "開始使用",
|
"getting_started.heading": "開始使用",
|
||||||
"getting_started.open_source_notice": "Mastodon 是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。你亦可透過{apps}閱讀 Mastodon 上的消息。",
|
"getting_started.open_source_notice": "Mastodon 是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。你亦可透過{apps}閱讀 Mastodon 上的消息。",
|
||||||
|
"home.column_settings.advanced": "進階",
|
||||||
"home.column_settings.basic": "基本",
|
"home.column_settings.basic": "基本",
|
||||||
|
"home.column_settings.filter_regex": "使用正規表達式 (regular expression) 過濾",
|
||||||
"home.column_settings.show_reblogs": "顯示被轉推的文章",
|
"home.column_settings.show_reblogs": "顯示被轉推的文章",
|
||||||
"home.column_settings.show_replies": "顯示回應文章",
|
"home.column_settings.show_replies": "顯示回應文章",
|
||||||
"home.column_settings.advanced": "進階",
|
"home.settings": "欄位設定",
|
||||||
"lightbox.close": "關閉",
|
"lightbox.close": "Close",
|
||||||
"loading_indicator.label": "載入中...",
|
"loading_indicator.label": "載入中...",
|
||||||
|
"media_gallery.toggle_visible": "打開或關上",
|
||||||
"missing_indicator.label": "找不到內容",
|
"missing_indicator.label": "找不到內容",
|
||||||
|
"navigation_bar.blocks": "被封鎖的用戶",
|
||||||
"navigation_bar.community_timeline": "本站時間軸",
|
"navigation_bar.community_timeline": "本站時間軸",
|
||||||
"navigation_bar.edit_profile": "修改個人資料",
|
"navigation_bar.edit_profile": "修改個人資料",
|
||||||
|
"navigation_bar.favourites": "喜歡的內容",
|
||||||
|
"navigation_bar.follow_requests": "關注請求",
|
||||||
|
"navigation_bar.info": "關於本服務站",
|
||||||
"navigation_bar.logout": "登出",
|
"navigation_bar.logout": "登出",
|
||||||
"navigation_bar.preferences": "個人設定",
|
"navigation_bar.preferences": "偏好設定",
|
||||||
"navigation_bar.public_timeline": "跨站公共時間軸",
|
"navigation_bar.public_timeline": "跨站公共時間軸",
|
||||||
"notification.favourite": "{name} 喜歡你的文章",
|
"notification.favourite": "{name} 喜歡你的文章",
|
||||||
"notification.follow": "{name} 開始開始你",
|
"notification.follow": "{name} 開始關注你",
|
||||||
"notification.mention": "{name} 提及你",
|
"notification.mention": "{name} 提及你",
|
||||||
"notification.reblog": "{name} 轉推你的文章",
|
"notification.reblog": "{name} 轉推你的文章",
|
||||||
|
"notifications.clear_confirmation": "你確定要清空通知紀錄嗎?",
|
||||||
|
"notifications.clear": "清空通知紀錄",
|
||||||
"notifications.column_settings.alert": "顯示桌面通知",
|
"notifications.column_settings.alert": "顯示桌面通知",
|
||||||
"notifications.column_settings.favourite": "喜歡你的文章:",
|
"notifications.column_settings.favourite": "喜歡你的文章:",
|
||||||
"notifications.column_settings.follow": "關注你:",
|
"notifications.column_settings.follow": "關注你:",
|
||||||
|
@ -75,13 +97,26 @@ const zh_hk = {
|
||||||
"notifications.column_settings.reblog": "轉推你的文章:",
|
"notifications.column_settings.reblog": "轉推你的文章:",
|
||||||
"notifications.column_settings.show": "在通知欄顯示",
|
"notifications.column_settings.show": "在通知欄顯示",
|
||||||
"notifications.column_settings.sound": "播放音效",
|
"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": "取消",
|
"reply_indicator.cancel": "取消",
|
||||||
|
"report.heading": "舉報",
|
||||||
|
"report.placeholder": "額外訊息",
|
||||||
|
"report.submit": "提交",
|
||||||
"report.target": "Reporting",
|
"report.target": "Reporting",
|
||||||
|
"search_results.total": "{count} 項結果",
|
||||||
"search.account": "用戶",
|
"search.account": "用戶",
|
||||||
"search.hashtag": "標籤",
|
"search.hashtag": "標籤",
|
||||||
"search.placeholder": "搜尋",
|
"search.placeholder": "搜尋",
|
||||||
"search_results.total": "{count} 項結果",
|
"search.status_by": "按{name}搜尋文章",
|
||||||
"search.status_by": "按用戶名稱搜尋文章",
|
|
||||||
"status.delete": "刪除",
|
"status.delete": "刪除",
|
||||||
"status.favourite": "喜歡",
|
"status.favourite": "喜歡",
|
||||||
"status.load_more": "載入更多",
|
"status.load_more": "載入更多",
|
||||||
|
@ -97,17 +132,19 @@ const zh_hk = {
|
||||||
"status.show_less": "減少顯示",
|
"status.show_less": "減少顯示",
|
||||||
"status.show_more": "顯示更多",
|
"status.show_more": "顯示更多",
|
||||||
"tabs_bar.compose": "撰寫",
|
"tabs_bar.compose": "撰寫",
|
||||||
"tabs_bar.home": "家",
|
"tabs_bar.federated_timeline": "跨站",
|
||||||
|
"tabs_bar.home": "主頁",
|
||||||
"tabs_bar.local_timeline": "本站",
|
"tabs_bar.local_timeline": "本站",
|
||||||
"tabs_bar.mentions": "提及",
|
"tabs_bar.mentions": "提及",
|
||||||
"tabs_bar.notifications": "通知",
|
"tabs_bar.notifications": "通知",
|
||||||
"tabs_bar.public": "跨站公共時間軸",
|
"tabs_bar.public": "跨站公共時間軸",
|
||||||
"tabs_bar.federated_timeline": "跨站",
|
|
||||||
"upload_area.title": "將檔案拖放至此上載",
|
"upload_area.title": "將檔案拖放至此上載",
|
||||||
"upload_button.label": "上載媒體檔案",
|
"upload_button.label": "上載媒體檔案",
|
||||||
"upload_progress.label": "上載中……",
|
|
||||||
"upload_form.undo": "還原",
|
"upload_form.undo": "還原",
|
||||||
|
"upload_progress.label": "上載中……",
|
||||||
|
"video_player.expand": "展開影片",
|
||||||
"video_player.toggle_sound": "開關音效",
|
"video_player.toggle_sound": "開關音效",
|
||||||
|
"video_player.toggle_visible": "打開或關上",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default zh_hk;
|
export default zh_hk;
|
||||||
|
|
|
@ -22,7 +22,7 @@ export default function errorsMiddleware() {
|
||||||
|
|
||||||
dispatch(showAlert(title, message));
|
dispatch(showAlert(title, message));
|
||||||
} else {
|
} else {
|
||||||
console.error(action.error);
|
console.error(action.error); // eslint-disable-line no-console
|
||||||
dispatch(showAlert('Oops!', 'An unexpected error occurred.'));
|
dispatch(showAlert('Oops!', 'An unexpected error occurred.'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,10 @@ import {
|
||||||
BLOCKS_FETCH_SUCCESS,
|
BLOCKS_FETCH_SUCCESS,
|
||||||
BLOCKS_EXPAND_SUCCESS
|
BLOCKS_EXPAND_SUCCESS
|
||||||
} from '../actions/blocks';
|
} from '../actions/blocks';
|
||||||
|
import {
|
||||||
|
MUTES_FETCH_SUCCESS,
|
||||||
|
MUTES_EXPAND_SUCCESS
|
||||||
|
} from '../actions/mutes';
|
||||||
import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
|
import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
|
||||||
import {
|
import {
|
||||||
REBLOG_SUCCESS,
|
REBLOG_SUCCESS,
|
||||||
|
@ -94,6 +98,8 @@ export default function accounts(state = initialState, action) {
|
||||||
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
||||||
case BLOCKS_FETCH_SUCCESS:
|
case BLOCKS_FETCH_SUCCESS:
|
||||||
case BLOCKS_EXPAND_SUCCESS:
|
case BLOCKS_EXPAND_SUCCESS:
|
||||||
|
case MUTES_FETCH_SUCCESS:
|
||||||
|
case MUTES_EXPAND_SUCCESS:
|
||||||
return normalizeAccounts(state, action.accounts);
|
return normalizeAccounts(state, action.accounts);
|
||||||
case NOTIFICATIONS_REFRESH_SUCCESS:
|
case NOTIFICATIONS_REFRESH_SUCCESS:
|
||||||
case NOTIFICATIONS_EXPAND_SUCCESS:
|
case NOTIFICATIONS_EXPAND_SUCCESS:
|
||||||
|
|
|
@ -9,17 +9,17 @@ const initialState = Immutable.List([]);
|
||||||
|
|
||||||
export default function alerts(state = initialState, action) {
|
export default function alerts(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case ALERT_SHOW:
|
case ALERT_SHOW:
|
||||||
return state.push(Immutable.Map({
|
return state.push(Immutable.Map({
|
||||||
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
||||||
title: action.title,
|
title: action.title,
|
||||||
message: action.message
|
message: action.message
|
||||||
}));
|
}));
|
||||||
case ALERT_DISMISS:
|
case ALERT_DISMISS:
|
||||||
return state.filterNot(item => item.get('key') === action.alert.key);
|
return state.filterNot(item => item.get('key') === action.alert.key);
|
||||||
case ALERT_CLEAR:
|
case ALERT_CLEAR:
|
||||||
return state.clear();
|
return state.clear();
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { STORE_HYDRATE } from '../actions/store';
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
const initialState = Immutable.Map({
|
const initialState = Immutable.Map({
|
||||||
|
streaming_api_base_url: null,
|
||||||
access_token: null,
|
access_token: null,
|
||||||
me: null
|
me: null
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,6 +16,10 @@ import {
|
||||||
BLOCKS_FETCH_SUCCESS,
|
BLOCKS_FETCH_SUCCESS,
|
||||||
BLOCKS_EXPAND_SUCCESS
|
BLOCKS_EXPAND_SUCCESS
|
||||||
} from '../actions/blocks';
|
} from '../actions/blocks';
|
||||||
|
import {
|
||||||
|
MUTES_FETCH_SUCCESS,
|
||||||
|
MUTES_EXPAND_SUCCESS
|
||||||
|
} from '../actions/mutes';
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
const initialState = Immutable.Map({
|
const initialState = Immutable.Map({
|
||||||
|
@ -24,7 +28,8 @@ const initialState = Immutable.Map({
|
||||||
reblogged_by: Immutable.Map(),
|
reblogged_by: Immutable.Map(),
|
||||||
favourited_by: Immutable.Map(),
|
favourited_by: Immutable.Map(),
|
||||||
follow_requests: Immutable.Map(),
|
follow_requests: Immutable.Map(),
|
||||||
blocks: Immutable.Map()
|
blocks: Immutable.Map(),
|
||||||
|
mutes: Immutable.Map()
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeList = (state, type, id, accounts, next) => {
|
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);
|
return state.setIn(['blocks', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
|
||||||
case BLOCKS_EXPAND_SUCCESS:
|
case BLOCKS_EXPAND_SUCCESS:
|
||||||
return state.updateIn(['blocks', 'items'], list => list.push(...action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
|
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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ const createWebSocketURL = (url) => {
|
||||||
return a.href;
|
return a.href;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function getStream(accessToken, stream, { connected, received, disconnected, reconnected }) {
|
export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
|
||||||
const ws = new WebSocketClient(`${createWebSocketURL(STREAMING_API_BASE_URL)}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
|
const ws = new WebSocketClient(`${createWebSocketURL(streamingAPIBaseURL)}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
|
||||||
|
|
||||||
ws.onopen = connected;
|
ws.onopen = connected;
|
||||||
ws.onmessage = e => received(JSON.parse(e.data));
|
ws.onmessage = e => received(JSON.parse(e.data));
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
p, li {
|
p, li {
|
||||||
font: 16px/28px 'Montserrat', sans-serif;
|
font: 16px/28px 'Montserrat', sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
margin-bottom: 26px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $color4;
|
color: $color4;
|
||||||
|
|
|
@ -2056,3 +2056,11 @@ button.icon-button.active i.fa-retweet {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-bar {
|
||||||
|
background-color: $color4;
|
||||||
|
height: 3px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -6,3 +6,12 @@
|
||||||
margin: 0 5px;
|
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)
|
def stream_entry_from_url(url)
|
||||||
params = Rails.application.routes.recognize_path(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])
|
StreamEntry.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,7 +30,7 @@ class Api::PushController < ApiController
|
||||||
params = Rails.application.routes.recognize_path(uri.path)
|
params = Rails.application.routes.recognize_path(uri.path)
|
||||||
domain = uri.host + (uri.port ? ":#{uri.port}" : '')
|
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])
|
Account.find_local(params[:username])
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,7 @@ class ApplicationController < ActionController::Base
|
||||||
force_ssl if: "Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'"
|
force_ssl if: "Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'"
|
||||||
|
|
||||||
include Localized
|
include Localized
|
||||||
helper_method :current_account
|
helper_method :current_account, :single_user_mode?
|
||||||
|
|
||||||
rescue_from ActionController::RoutingError, with: :not_found
|
rescue_from ActionController::RoutingError, with: :not_found
|
||||||
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
||||||
|
@ -69,6 +69,10 @@ class ApplicationController < ActionController::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def single_user_mode?
|
||||||
|
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.first
|
||||||
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
@current_account ||= current_user.try(:account)
|
@current_account ||= current_user.try(:account)
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,7 +28,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_enabled_registrations
|
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -49,7 +49,8 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_otp_attempt?(user)
|
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
|
end
|
||||||
|
|
||||||
def authenticate_with_two_factor
|
def authenticate_with_two_factor
|
||||||
|
|
|
@ -27,7 +27,11 @@ module Localized
|
||||||
|
|
||||||
def default_locale
|
def default_locale
|
||||||
ENV.fetch('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
|
end
|
||||||
|
|
||||||
|
def user_supplied_locale
|
||||||
|
http_accept_language.language_region_compatible_from(I18n.available_locales)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,15 +4,16 @@ class HomeController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@body_classes = 'app-body'
|
@body_classes = 'app-body'
|
||||||
@token = find_or_create_access_token.token
|
@token = find_or_create_access_token.token
|
||||||
@web_settings = Web::Setting.find_by(user: current_user)&.data || {}
|
@web_settings = Web::Setting.find_by(user: current_user)&.data || {}
|
||||||
|
@streaming_api_base_url = Rails.configuration.x.streaming_api_base_url
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def authenticate_user!
|
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
|
end
|
||||||
|
|
||||||
def find_or_create_access_token
|
def find_or_create_access_token
|
||||||
|
|
|
@ -19,9 +19,9 @@ class Settings::TwoFactorAuthsController < ApplicationController
|
||||||
def create
|
def create
|
||||||
if current_user.validate_and_consume_otp!(confirmation_params[:code])
|
if current_user.validate_and_consume_otp!(confirmation_params[:code])
|
||||||
current_user.otp_required_for_login = true
|
current_user.otp_required_for_login = true
|
||||||
|
@codes = current_user.generate_otp_backup_codes!
|
||||||
current_user.save!
|
current_user.save!
|
||||||
|
flash[:notice] = I18n.t('two_factor_auth.enabled_success')
|
||||||
redirect_to settings_two_factor_auth_path, notice: I18n.t('two_factor_auth.enabled_success')
|
|
||||||
else
|
else
|
||||||
@confirmation = Form::TwoFactorConfirmation.new
|
@confirmation = Form::TwoFactorConfirmation.new
|
||||||
set_qr_code
|
set_qr_code
|
||||||
|
@ -30,6 +30,12 @@ class Settings::TwoFactorAuthsController < ApplicationController
|
||||||
end
|
end
|
||||||
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
|
def disable
|
||||||
current_user.otp_required_for_login = false
|
current_user.otp_required_for_login = false
|
||||||
current_user.save!
|
current_user.save!
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Admin::AccountsHelper
|
module Admin::FilterHelper
|
||||||
def filter_params(more_params)
|
ACCOUNT_FILTERS = %i[local remote by_domain silenced suspended recent].freeze
|
||||||
params.permit(:local, :remote, :by_domain, :silenced, :suspended, :recent, :resolved).merge(more_params)
|
REPORT_FILTERS = %i[resolved].freeze
|
||||||
end
|
|
||||||
|
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS
|
||||||
|
|
||||||
def filter_link_to(text, more_params)
|
def filter_link_to(text, more_params)
|
||||||
new_url = filtered_url_for(more_params)
|
new_url = filtered_url_for(more_params)
|
||||||
|
@ -16,6 +17,10 @@ module Admin::AccountsHelper
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def filter_params(more_params)
|
||||||
|
params.permit(FILTERS).merge(more_params)
|
||||||
|
end
|
||||||
|
|
||||||
def filter_link_class(new_url)
|
def filter_link_class(new_url)
|
||||||
filtered_url_for(params) == new_url ? 'selected' : ''
|
filtered_url_for(params) == new_url ? 'selected' : ''
|
||||||
end
|
end
|
|
@ -56,6 +56,10 @@ class TagManager
|
||||||
id.start_with?("tag:#{Rails.configuration.x.local_domain}")
|
id.start_with?("tag:#{Rails.configuration.x.local_domain}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def web_domain?(domain)
|
||||||
|
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.web_domain).zero?
|
||||||
|
end
|
||||||
|
|
||||||
def local_domain?(domain)
|
def local_domain?(domain)
|
||||||
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.local_domain).zero?
|
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.local_domain).zero?
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,9 @@ class User < ApplicationRecord
|
||||||
|
|
||||||
devise :registerable, :recoverable,
|
devise :registerable, :recoverable,
|
||||||
:rememberable, :trackable, :validatable, :confirmable,
|
: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
|
belongs_to :account, inverse_of: :user
|
||||||
accepts_nested_attributes_for :account
|
accepts_nested_attributes_for :account
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class InstancePresenter
|
class InstancePresenter
|
||||||
delegate(
|
delegate(
|
||||||
:closed_registrations_message,
|
:closed_registrations_message,
|
||||||
:contact_email,
|
:site_contact_email,
|
||||||
:open_registrations,
|
:open_registrations,
|
||||||
:site_description,
|
:site_description,
|
||||||
:site_extended_description,
|
:site_extended_description,
|
||||||
|
|
|
@ -1,26 +1,86 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AccountSearchService < BaseService
|
class AccountSearchService < BaseService
|
||||||
|
attr_reader :query, :limit, :resolve, :account
|
||||||
|
|
||||||
def call(query, limit, resolve = false, account = nil)
|
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('@')
|
search_service_results
|
||||||
domain = nil if TagManager.instance.local_domain?(domain)
|
end
|
||||||
|
|
||||||
if domain.nil?
|
private
|
||||||
exact_match = Account.find_local(username)
|
|
||||||
results = account.nil? ? Account.search_for(username, limit) : Account.advanced_search_for(username, account, limit)
|
def search_service_results
|
||||||
|
return [] if query_blank_or_hashtag?
|
||||||
|
|
||||||
|
if resolving_non_matching_remote_account?
|
||||||
|
[FollowRemoteAccountService.new.call("#{query_username}@#{query_domain}")]
|
||||||
else
|
else
|
||||||
exact_match = Account.find_remote(username, domain)
|
search_results_and_exact_match.compact.uniq
|
||||||
results = account.nil? ? Account.search_for("#{username} #{domain}", limit) : Account.advanced_search_for("#{username} #{domain}", account, limit)
|
|
||||||
end
|
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?
|
def search_results_and_exact_match
|
||||||
results = [FollowRemoteAccountService.new.call("#{username}@#{domain}")]
|
[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
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,7 @@ class FollowRemoteAccountService < BaseService
|
||||||
return Account.find_local(username) if TagManager.instance.local_domain?(domain)
|
return Account.find_local(username) if TagManager.instance.local_domain?(domain)
|
||||||
|
|
||||||
account = Account.find_remote(username, 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}"
|
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)
|
return Account.find_local(confirmed_username) if TagManager.instance.local_domain?(confirmed_domain)
|
||||||
|
|
||||||
confirmed_account = Account.find_remote(confirmed_username, 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.remote_url = data.link('http://schemas.google.com/g/2010#updates-from').href
|
||||||
account.salmon_url = data.link('salmon').href
|
account.salmon_url = data.link('salmon').href
|
||||||
account.url = data.link('http://webfinger.net/rel/profile-page').href
|
account.url = data.link('http://webfinger.net/rel/profile-page').href
|
||||||
account.public_key = magic_key_to_pem(data.link('magic-public-key').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)
|
body, xml = get_feed(account.remote_url)
|
||||||
hubs = get_hubs(xml)
|
hubs = get_hubs(xml)
|
||||||
|
|
|
@ -164,7 +164,7 @@ class ProcessFeedService < BaseService
|
||||||
|
|
||||||
url = Addressable::URI.parse(link['href'])
|
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/', ''))
|
Account.find_local(url.path.gsub('/users/', ''))
|
||||||
else
|
else
|
||||||
Account.find_by(url: link['href']) || FetchRemoteAccountService.new.call(link['href'])
|
Account.find_by(url: link['href']) || FetchRemoteAccountService.new.call(link['href'])
|
||||||
|
|
|
@ -4,7 +4,7 @@ class Pubsubhubbub::UnsubscribeService < BaseService
|
||||||
def call(account, callback)
|
def call(account, callback)
|
||||||
return ['Invalid topic URL', 422] if account.nil?
|
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?
|
unless subscription.nil?
|
||||||
Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'unsubscribe')
|
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
|
.panel= @instance_presenter.site_extended_description.html_safe
|
||||||
|
|
||||||
.sidebar
|
.sidebar
|
||||||
.panel
|
= render partial: 'contact', object: @instance_presenter
|
||||||
.panel-header= t 'about.contact'
|
= render 'links'
|
||||||
.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'
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
.wrapper
|
.wrapper
|
||||||
%h1
|
%h1
|
||||||
= image_tag 'logo.png'
|
= image_tag 'logo.png'
|
||||||
Mastodon
|
= Setting.site_title
|
||||||
|
|
||||||
%p= t('about.about_mastodon').html_safe
|
%p= t('about.about_mastodon').html_safe
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
%meta{ property: 'og:image:height', content: '120' }/
|
%meta{ property: 'og:image:height', content: '120' }/
|
||||||
%meta{ property: 'twitter:card', content: 'summary' }/
|
%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 }
|
= render partial: 'shared/landing_strip', locals: { account: @account }
|
||||||
|
|
||||||
.h-feed
|
.h-feed
|
||||||
|
|
|
@ -61,12 +61,16 @@
|
||||||
= surround '(', ')' do
|
= surround '(', ')' do
|
||||||
= number_to_human_size @account.media_attachments.sum('file_file_size')
|
= number_to_human_size @account.media_attachments.sum('file_file_size')
|
||||||
|
|
||||||
- if @account.silenced?
|
%div{ style: 'float: right' }
|
||||||
= link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button'
|
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button'
|
||||||
- else
|
|
||||||
= link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button'
|
|
||||||
|
|
||||||
- if @account.suspended?
|
%div{ style: 'float: left' }
|
||||||
= link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button'
|
- if @account.silenced?
|
||||||
- else
|
= link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button'
|
||||||
= 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'
|
- 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')
|
= t('auth.login')
|
||||||
|
|
||||||
= simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
|
= 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
|
.actions
|
||||||
= f.button :button, t('auth.login'), type: :submit
|
= f.button :button, t('auth.login'), type: :submit
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
:javascript
|
%script#initial-state{:type => 'application/json'}!= json_escape(render(file: 'home/initial_state', formats: :json))
|
||||||
window.STREAMING_API_BASE_URL = '#{Rails.configuration.x.streaming_api_base_url}';
|
|
||||||
window.INITIAL_STATE = #{json_escape(render(file: 'home/initial_state', formats: :json))}
|
|
||||||
|
|
||||||
= javascript_include_tag 'application', integrity: true
|
= javascript_include_tag 'application', integrity: true
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ object false
|
||||||
|
|
||||||
node(:meta) do
|
node(:meta) do
|
||||||
{
|
{
|
||||||
|
streaming_api_base_url: @streaming_api_base_url,
|
||||||
access_token: @token,
|
access_token: @token,
|
||||||
locale: I18n.locale,
|
locale: I18n.locale,
|
||||||
me: current_account.id,
|
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'
|
= link_to t('two_factor_auth.disable'), disable_settings_two_factor_auth_path, data: { method: 'POST' }, class: 'block-button'
|
||||||
- else
|
- else
|
||||||
= link_to t('two_factor_auth.setup'), new_settings_two_factor_auth_path, class: 'block-button'
|
= 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' }/
|
%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 }
|
= render partial: 'shared/landing_strip', locals: { account: @stream_entry.account }
|
||||||
|
|
||||||
.activity-stream.activity-stream-headless.h-entry
|
.activity-stream.activity-stream-headless.h-entry
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
.compact-header
|
.compact-header
|
||||||
%h1<
|
%h1<
|
||||||
= link_to 'Mastodon', root_path
|
= link_to site_title, root_path
|
||||||
%small= "##{@tag.name}"
|
%small= "##{@tag.name}"
|
||||||
|
|
||||||
- if @statuses.empty?
|
- if @statuses.empty?
|
||||||
|
|
|
@ -42,6 +42,7 @@ module Mastodon
|
||||||
:uk,
|
:uk,
|
||||||
'zh-CN',
|
'zh-CN',
|
||||||
:'zh-HK',
|
:'zh-HK',
|
||||||
|
:'zh-TW',
|
||||||
]
|
]
|
||||||
|
|
||||||
config.i18n.default_locale = :en
|
config.i18n.default_locale = :en
|
||||||
|
|
|
@ -55,6 +55,8 @@ Rails.application.configure do
|
||||||
ENV['REDIS_HOST'] = redis_url.host
|
ENV['REDIS_HOST'] = redis_url.host
|
||||||
ENV['REDIS_PORT'] = redis_url.port.to_s
|
ENV['REDIS_PORT'] = redis_url.port.to_s
|
||||||
ENV['REDIS_PASSWORD'] = redis_url.password
|
ENV['REDIS_PASSWORD'] = redis_url.password
|
||||||
|
db_num = redis_url.path[1..-1]
|
||||||
|
ENV['REDIS_DB'] = db_num if db_num.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Use a different cache store in production.
|
# Use a different cache store in production.
|
||||||
|
@ -62,7 +64,7 @@ Rails.application.configure do
|
||||||
host: ENV.fetch('REDIS_HOST') { 'localhost' },
|
host: ENV.fetch('REDIS_HOST') { 'localhost' },
|
||||||
port: ENV.fetch('REDIS_PORT') { 6379 },
|
port: ENV.fetch('REDIS_PORT') { 6379 },
|
||||||
password: ENV.fetch('REDIS_PASSWORD') { false },
|
password: ENV.fetch('REDIS_PASSWORD') { false },
|
||||||
db: 0,
|
db: ENV.fetch('REDIS_DB') { 0 },
|
||||||
namespace: 'cache',
|
namespace: 'cache',
|
||||||
expires_in: 10.minutes,
|
expires_in: 10.minutes,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
Devise.setup do |config|
|
Devise.setup do |config|
|
||||||
config.warden do |manager|
|
config.warden do |manager|
|
||||||
manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
|
manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
|
||||||
|
manager.default_strategies(scope: :user).unshift :two_factor_backupable
|
||||||
end
|
end
|
||||||
|
|
||||||
# The secret key used by Devise. Devise uses this key to generate
|
# The secret key used by Devise. Devise uses this key to generate
|
||||||
|
|
|
@ -2,18 +2,20 @@
|
||||||
|
|
||||||
port = ENV.fetch('PORT') { 3000 }
|
port = ENV.fetch('PORT') { 3000 }
|
||||||
host = ENV.fetch('LOCAL_DOMAIN') { "localhost:#{port}" }
|
host = ENV.fetch('LOCAL_DOMAIN') { "localhost:#{port}" }
|
||||||
|
web_host = ENV.fetch('WEB_DOMAIN') { host }
|
||||||
https = ENV['LOCAL_HTTPS'] == 'true'
|
https = ENV['LOCAL_HTTPS'] == 'true'
|
||||||
|
|
||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
config.x.local_domain = host
|
config.x.local_domain = host
|
||||||
|
config.x.web_domain = web_host
|
||||||
config.x.use_https = https
|
config.x.use_https = https
|
||||||
config.x.use_s3 = ENV['S3_ENABLED'] == 'true'
|
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'
|
config.x.streaming_api_base_url = 'http://localhost:4000'
|
||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
config.action_cable.allowed_request_origins = ["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' : ''}://#{host}" }
|
config.x.streaming_api_base_url = ENV.fetch('STREAMING_API_BASE_URL') { "http#{https ? 's' : ''}://#{web_host}" }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -38,4 +38,7 @@ if ENV['S3_ENABLED'] == 'true'
|
||||||
Paperclip::Attachment.default_options[:url] = ':s3_alias_url'
|
Paperclip::Attachment.default_options[:url] = ':s3_alias_url'
|
||||||
Paperclip::Attachment.default_options[:s3_host_alias] = ENV['S3_CLOUDFRONT_HOST']
|
Paperclip::Attachment.default_options[:s3_host_alias] = ENV['S3_CLOUDFRONT_HOST']
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
host = ENV.fetch('REDIS_HOST') { 'localhost' }
|
host = ENV.fetch('REDIS_HOST') { 'localhost' }
|
||||||
port = ENV.fetch('REDIS_PORT') { 6379 }
|
port = ENV.fetch('REDIS_PORT') { 6379 }
|
||||||
password = ENV.fetch('REDIS_PASSWORD') { false }
|
password = ENV.fetch('REDIS_PASSWORD') { false }
|
||||||
|
db = ENV.fetch('REDIS_DB') { 0 }
|
||||||
|
|
||||||
Sidekiq.configure_server do |config|
|
Sidekiq.configure_server do |config|
|
||||||
config.redis = { host: host, port: port, password: password}
|
config.redis = { host: host, port: port, db: db, password: password }
|
||||||
end
|
end
|
||||||
|
|
||||||
Sidekiq.configure_client do |config|
|
Sidekiq.configure_client do |config|
|
||||||
config.redis = { host: host, port: port, password: password }
|
config.redis = { host: host, port: port, db: db, password: password }
|
||||||
end
|
end
|
||||||
|
|
|
@ -165,5 +165,3 @@ bg:
|
||||||
users:
|
users:
|
||||||
invalid_email: E-mail адресът е невалиден
|
invalid_email: E-mail адресът е невалиден
|
||||||
invalid_otp_token: Невалиден код
|
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_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.
|
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:
|
failure:
|
||||||
already_authenticated: Vous êtes déjà connecté
|
already_authenticated: Vous êtes déjà connecté⋅e
|
||||||
inactive: Votre compte n'est pas encore activé.
|
inactive: Votre compte n’est pas encore activé.
|
||||||
invalid: Email ou mot de passe incorrect.
|
invalid: Courriel ou mot de passe incorrect.
|
||||||
last_attempt: Vous avez droit à une tentative avant que votre compte ne soit verrouillé.
|
last_attempt: Vous avez droit à une tentative avant que votre compte ne soit verrouillé.
|
||||||
locked: Votre compte est 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.
|
timeout: Votre session a expiré. Veuillez vous reconnecter pour continuer.
|
||||||
unauthenticated: Vous devez vous connecter ou vous inscrire pour continuer.
|
unauthenticated: Vous devez vous connecter ou vous inscrire pour continuer.
|
||||||
unconfirmed: Vous devez valider votre compte pour continuer.
|
unconfirmed: Vous devez valider votre compte pour continuer.
|
||||||
|
@ -25,7 +25,7 @@ fr:
|
||||||
unlock_instructions:
|
unlock_instructions:
|
||||||
subject: Instructions pour déverrouiller votre compte
|
subject: Instructions pour déverrouiller votre compte
|
||||||
omniauth_callbacks:
|
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}.
|
success: Authentifié avec succès via %{kind}.
|
||||||
passwords:
|
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.
|
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:
|
registrations:
|
||||||
destroyed: Votre compte a été supprimé avec succès. Nous espérons vous revoir bientôt.
|
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: 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_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.
|
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 email. Merci de vérifier vos emails et de cliquer sur le lien de confirmation pour finaliser la validation de votre nouvelle adresse.
|
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.
|
updated: Votre compte a été modifié avec succès.
|
||||||
sessions:
|
sessions:
|
||||||
already_signed_out: Déconnecté.
|
already_signed_out: Déconnecté.
|
||||||
|
@ -47,15 +47,15 @@ fr:
|
||||||
signed_out: Déconnecté.
|
signed_out: Déconnecté.
|
||||||
unlocks:
|
unlocks:
|
||||||
send_instructions: Vous allez recevoir les instructions nécessaires au déverrouillage de votre compte dans quelques instants
|
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.
|
unlocked: Votre compte a été déverrouillé avec succès, vous êtes maintenant connecté⋅e.
|
||||||
errors:
|
errors:
|
||||||
messages:
|
messages:
|
||||||
already_confirmed: a déjà été validé⋅e, veuillez essayer de vous connecter
|
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
|
confirmation_period_expired: à confirmer dans les %{period}, merci de faire une nouvelle demande
|
||||||
expired: a expiré, merci d'en faire une nouvelle demande
|
expired: a expiré, merci d’en faire une nouvelle demande
|
||||||
not_found: n'a pas été trouvé⋅e
|
not_found: n’a pas été trouvé⋅e
|
||||||
not_locked: n'était pas verrouillé⋅e
|
not_locked: n’était pas verrouillé⋅e
|
||||||
not_saved:
|
not_saved:
|
||||||
one: '1 erreur a empêché ce(tte) %{resource} d’être sauvegardé⋅e :'
|
one: '1 erreur a empêché ce(tte) %{resource} d’être sauvegardé⋅e :'
|
||||||
other: '%{count} erreurs ont empêché %{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