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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -7,12 +7,13 @@ class HomeController < ApplicationController
|
||||||
@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?
|
||||||
if resolve && !exact_match && !domain.nil?
|
|
||||||
results = [FollowRemoteAccountService.new.call("#{username}@#{domain}")]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
results
|
def search_results_and_exact_match
|
||||||
|
[exact_match] + search_results.to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_blank_or_hashtag?
|
||||||
|
query.blank? || query.start_with?('#')
|
||||||
|
end
|
||||||
|
|
||||||
|
def split_query_string
|
||||||
|
@_split_query_string ||= query.gsub(/\A@/, '').split('@')
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_username
|
||||||
|
@_query_username ||= split_query_string.first
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_domain
|
||||||
|
@_query_domain ||= query_without_split? ? nil : split_query_string.last
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_without_split?
|
||||||
|
split_query_string.size == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def domain_is_local?
|
||||||
|
@_domain_is_local ||= TagManager.instance.local_domain?(query_domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
def exact_match
|
||||||
|
@_exact_match ||= Account.find_remote(query_username, query_domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
def search_results
|
||||||
|
@_search_results ||= if account
|
||||||
|
advanced_search_results
|
||||||
|
else
|
||||||
|
simple_search_results
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
domain_block = DomainBlock.find_by(domain: domain)
|
||||||
|
|
||||||
account = Account.new(username: confirmed_username, domain: confirmed_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
|
||||||
|
|
||||||
|
account.last_webfingered_at = Time.now.utc
|
||||||
|
|
||||||
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.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button'
|
||||||
|
|
||||||
|
%div{ style: 'float: left' }
|
||||||
|
- if @account.silenced?
|
||||||
= link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button'
|
= link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button'
|
||||||
- else
|
- else
|
||||||
= link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button'
|
= link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button'
|
||||||
|
|
||||||
- if @account.suspended?
|
- if @account.suspended?
|
||||||
= link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button'
|
= link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button'
|
||||||
- else
|
- 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'
|
= 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 :'
|
||||||
|
|
|
@ -2,60 +2,60 @@
|
||||||
'no':
|
'no':
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Epostaddressen din er blitt bekreftet.
|
confirmed: E-postaddressen din er blitt bekreftet.
|
||||||
send_instructions: Du vil motta en epost med instruksjoner for hvordan bekrefte din epostaddresse om noen få minutter.
|
send_instructions: Du vil motta en e-post med instruksjoner for bekreftelse om noen få minutter.
|
||||||
send_paranoid_instructions: Hvis din epostaddresse finnes i vår database vil du motta en epost med instruksjoner for hvordan bekrefte din epost om noen få minutter.
|
send_paranoid_instructions: Hvis din e-postaddresse finnes i vår database vil du motta en e-post med instruksjoner for bekreftelse om noen få minutter.
|
||||||
failure:
|
failure:
|
||||||
already_authenticated: Du er allerede innlogget.
|
already_authenticated: Du er allerede innlogget.
|
||||||
inactive: Din konto er ikke blitt aktivert ennå.
|
inactive: Din konto er ikke blitt aktivert ennå.
|
||||||
invalid: Ugyldig %{authentication_keys} eller passord.
|
invalid: Ugyldig %{authentication_keys} eller passord.
|
||||||
last_attempt: Du har ett forsøk igjen før kontoen din bli låst.
|
last_attempt: Du har ett forsøk igjen før kontoen din låses.
|
||||||
locked: Din konto er låst.
|
locked: Din konto er låst.
|
||||||
not_found_in_database: Ugyldig %{authentication_keys} eller passord.
|
not_found_in_database: Ugyldig %{authentication_keys} eller passord.
|
||||||
timeout: Sesjonen din løp ut på tid. Logg inn på nytt for å fortsette.
|
timeout: Økten din løp ut på tid. Logg inn på nytt for å fortsette.
|
||||||
unauthenticated: Du må logge inn eller registrere deg før du kan fortsette.
|
unauthenticated: Du må logge inn eller registrere deg før du kan fortsette.
|
||||||
unconfirmed: Du må bekrefte epostadressen din før du kan fortsette.
|
unconfirmed: Du må bekrefte e-postadressen din før du kan fortsette.
|
||||||
mailer:
|
mailer:
|
||||||
confirmation_instructions:
|
confirmation_instructions:
|
||||||
subject: 'Mastodon: Instruksjoner for å bekrefte epostadresse'
|
subject: 'Mastodon: Instruksjoner for å bekrefte e-postadresse'
|
||||||
password_change:
|
password_change:
|
||||||
subject: 'Mastodon: Passord endret'
|
subject: 'Mastodon: Passord endret'
|
||||||
reset_password_instructions:
|
reset_password_instructions:
|
||||||
subject: 'Mastodon: Hvordan nullstille passord?'
|
subject: 'Mastodon: Hvordan nullstille passord'
|
||||||
unlock_instructions:
|
unlock_instructions:
|
||||||
subject: 'Mastodon: Instruksjoner for å gjenåpne konto'
|
subject: 'Mastodon: Instruksjoner for å gjenåpne konto'
|
||||||
omniauth_callbacks:
|
omniauth_callbacks:
|
||||||
failure: Kunne ikke autentisere deg fra %{kind} fordi "%{reason}".
|
failure: Kunne ikke autentisere deg fra %{kind} fordi "%{reason}".
|
||||||
success: Vellykket autentisering fra %{kind}.
|
success: Vellykket autentisering fra %{kind}.
|
||||||
passwords:
|
passwords:
|
||||||
no_token: Du har ingen tilgang til denne siden så lenge du ikke kommer fra en epost om nullstilling av passord. Hvis du kommer fra en passordnullstilling epost, dobbelsjekk at du brukte hele URLen.
|
no_token: Du har ingen tilgang til denne siden hvis ikke klikket på en e-post om nullstilling av passord. Hvis du kommer fra en sådan bør du dobbelsjekke at du limte inn hele URLen.
|
||||||
send_instructions: Du vil motta en epost med instruksjoner for å nullstille passordet ditt om noen få minutter.
|
send_instructions: Du vil motta en e-post med instruksjoner om nullstilling av passord om noen få minutter.
|
||||||
send_paranoid_instructions: Hvis epostadressen din finnes i databasen vår vil du motta en instruksjonsmail om passord nullstilling om noen få minutter.
|
send_paranoid_instructions: Hvis e-postadressen din finnes i databasen vår vil du motta en e-post med instruksjoner om nullstilling av passord om noen få minutter.
|
||||||
updated: Passordet ditt har blitt endret. Du er nå logget inn.
|
updated: Passordet ditt er endret. Du er nå logget inn.
|
||||||
updated_not_active: Passordet ditt har blitt endret.
|
updated_not_active: Passordet ditt er endret.
|
||||||
registrations:
|
registrations:
|
||||||
destroyed: Adjø! Kontoen din har blitt avsluttet. Vi håper at vi ser deg igjen snart.
|
destroyed: Adjø! Kontoen din er slettet. På gjensyn!
|
||||||
signed_up: Velkommen! Registrasjonen var vellykket.
|
signed_up: Velkommen! Registreringen var vellykket.
|
||||||
signed_up_but_inactive: Registrasjonen var vellykket. Vi kunne dessverre ikke logge deg inn fordi kontoen din ennå ikke har blitt aktivert.
|
signed_up_but_inactive: Registreringen var vellykket. Vi kunne dessverre ikke logge deg inn fordi kontoen din ennå ikke har blitt aktivert.
|
||||||
signed_up_but_locked: Registrasjonen var vellykket. Vi kunne dessverre ikke logge deg inn fordi kontoen din har blitt låst.
|
signed_up_but_locked: Registreringen var vellykket. Vi kunne dessverre ikke logge deg inn fordi kontoen din har blitt låst.
|
||||||
signed_up_but_unconfirmed: En epostmelding med en bekreftelseslink har blitt sendt til din adresse. Klikk på linken i eposten for å aktivere kontoen din.
|
signed_up_but_unconfirmed: En e-post med en bekreftelseslenke har blitt sendt til din innboks. Klikk på lenken i e-posten for å aktivere kontoen din.
|
||||||
update_needs_confirmation: Du har oppdatert kontoen din, men vi må bekrefte din nye epostadresse. Sjekk eposten din og følg bekreftelseslinken for å bekrefte din nye epostadresse.
|
update_needs_confirmation: Du har oppdatert kontoen din, men vi må bekrefte din nye e-postadresse. Sjekk e-posten din og følg bekreftelseslenken for å bekrefte din nye e-postadresse.
|
||||||
updated: Kontoen din ble oppdatert.
|
updated: Kontoen din ble oppdatert.
|
||||||
sessions:
|
sessions:
|
||||||
already_signed_out: Logget ut.
|
already_signed_out: Logget ut.
|
||||||
signed_in: Logget inn.
|
signed_in: Logget inn.
|
||||||
signed_out: Logget ut.
|
signed_out: Logget ut.
|
||||||
unlocks:
|
unlocks:
|
||||||
send_instructions: Du vil motta en epost med instruksjoner for å åpne kontoen din om noen få minutter.
|
send_instructions: Du vil motta en e-post med instruksjoner for å åpne kontoen din om noen få minutter.
|
||||||
send_paranoid_instructions: Hvis kontoen din eksisterer vil du motta en epost med instruksjoner for å åpne kontoen din om noen få minutter.
|
send_paranoid_instructions: Hvis kontoen din eksisterer vil du motta en e-post med instruksjoner for å åpne kontoen din om noen få minutter.
|
||||||
unlocked: Kontoen din ble åpnet uten problemer. Logg på for å fortsette.
|
unlocked: Kontoen din ble åpnet uten problemer. Logg på for å fortsette.
|
||||||
errors:
|
errors:
|
||||||
messages:
|
messages:
|
||||||
already_confirmed: har allerede blitt bekreftet, prøv å logg på istedet.
|
already_confirmed: har allerede blitt bekreftet, prøv å logge på istedet.
|
||||||
confirmation_period_expired: må bekreftes innen %{period}. Spør om en ny bekreftelsesmail istedet.
|
confirmation_period_expired: må bekreftes innen %{period}. Spør om en ny e-mail for bekreftelse istedet.
|
||||||
expired: har utløpt, spør om en ny en istedet
|
expired: har utløpt, spør om en ny en istedet
|
||||||
not_found: ikke funnet
|
not_found: ikke funnet
|
||||||
not_locked: var ikke låst
|
not_locked: var ikke låst
|
||||||
not_saved:
|
not_saved:
|
||||||
one: '1 feil hindret denne %{resource} fra å bli lagret:'
|
one: '1 feil hindret denne %{resource} i å bli lagret:'
|
||||||
other: "%{count} feil hindret denne %{resource} fra å bli lagret:"
|
other: "%{count} feil hindret denne %{resource} i å bli lagret:"
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue