mirror of
https://git.kescher.at/CatCatNya/catstodon.git
synced 2024-11-22 22:18:06 +01:00
Merge pull request #2517 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 3bf896c973
This commit is contained in:
commit
c8fe36c349
116 changed files with 1403 additions and 1185 deletions
|
@ -1,5 +1,5 @@
|
||||||
# Node.js
|
# In test, compile the NodeJS code as if we are in production
|
||||||
NODE_ENV=tests
|
NODE_ENV=production
|
||||||
# Federation
|
# Federation
|
||||||
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||||
LOCAL_HTTPS=true
|
LOCAL_HTTPS=true
|
||||||
|
|
21
.github/workflows/test-ruby.yml
vendored
21
.github/workflows/test-ruby.yml
vendored
|
@ -48,12 +48,15 @@ jobs:
|
||||||
run: |-
|
run: |-
|
||||||
./bin/rails assets:precompile
|
./bin/rails assets:precompile
|
||||||
|
|
||||||
|
- name: Archive asset artifacts
|
||||||
|
run: |
|
||||||
|
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs*
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
if: matrix.mode == 'test'
|
if: matrix.mode == 'test'
|
||||||
with:
|
with:
|
||||||
path: |-
|
path: |-
|
||||||
./public/assets
|
./artifacts.tar.gz
|
||||||
./public/packs-test
|
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
retention-days: 0
|
retention-days: 0
|
||||||
|
|
||||||
|
@ -102,7 +105,6 @@ jobs:
|
||||||
SAML_ENABLED: true
|
SAML_ENABLED: true
|
||||||
CAS_ENABLED: true
|
CAS_ENABLED: true
|
||||||
BUNDLE_WITH: 'pam_authentication test'
|
BUNDLE_WITH: 'pam_authentication test'
|
||||||
CI_JOBS: ${{ matrix.ci_job }}/4
|
|
||||||
GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }}
|
GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
|
@ -112,19 +114,18 @@ jobs:
|
||||||
- '3.0'
|
- '3.0'
|
||||||
- '3.1'
|
- '3.1'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
ci_job:
|
|
||||||
- 1
|
|
||||||
- 2
|
|
||||||
- 3
|
|
||||||
- 4
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: './public'
|
path: './'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Expand archived asset artifacts
|
||||||
|
run: |
|
||||||
|
tar xvzf artifacts.tar.gz
|
||||||
|
|
||||||
- name: Set up Ruby environment
|
- name: Set up Ruby environment
|
||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
with:
|
with:
|
||||||
|
@ -134,7 +135,7 @@ jobs:
|
||||||
- name: Load database schema
|
- name: Load database schema
|
||||||
run: './bin/rails db:create db:schema:load db:seed'
|
run: './bin/rails db:create db:schema:load db:seed'
|
||||||
|
|
||||||
- run: bundle exec rake rspec_chunked
|
- run: bin/rspec
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
name: End to End testing
|
name: End to End testing
|
||||||
|
|
|
@ -27,7 +27,7 @@ AllCops:
|
||||||
- 'node_modules/**/*'
|
- 'node_modules/**/*'
|
||||||
- 'Vagrantfile'
|
- 'Vagrantfile'
|
||||||
- 'vendor/**/*'
|
- 'vendor/**/*'
|
||||||
- 'lib/json_ld/*' # Generated files
|
- 'config/initializers/json_ld*' # Generated files
|
||||||
- 'lib/mastodon/migration_helpers.rb' # Vendored from GitLab
|
- 'lib/mastodon/migration_helpers.rb' # Vendored from GitLab
|
||||||
- 'lib/templates/**/*'
|
- 'lib/templates/**/*'
|
||||||
|
|
||||||
|
|
5
Gemfile
5
Gemfile
|
@ -88,7 +88,7 @@ gem 'simple-navigation', '~> 4.4'
|
||||||
gem 'simple_form', '~> 5.2'
|
gem 'simple_form', '~> 5.2'
|
||||||
gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
|
gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
|
||||||
gem 'stoplight', '~> 3.0.1'
|
gem 'stoplight', '~> 3.0.1'
|
||||||
gem 'strong_migrations', '~> 0.8'
|
gem 'strong_migrations', '1.3.0'
|
||||||
gem 'tty-prompt', '~> 0.23', require: false
|
gem 'tty-prompt', '~> 0.23', require: false
|
||||||
gem 'twitter-text', '~> 3.1.0'
|
gem 'twitter-text', '~> 3.1.0'
|
||||||
gem 'tzinfo-data', '~> 1.2023'
|
gem 'tzinfo-data', '~> 1.2023'
|
||||||
|
@ -103,9 +103,6 @@ gem 'rdf-normalize', '~> 0.5'
|
||||||
gem 'private_address_check', '~> 0.5'
|
gem 'private_address_check', '~> 0.5'
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
# Used to split testing into chunks in CI
|
|
||||||
gem 'rspec_chunked', '~> 0.6'
|
|
||||||
|
|
||||||
# Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab
|
# Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab
|
||||||
gem 'rspec-github', '~> 2.4', require: false
|
gem 'rspec-github', '~> 2.4', require: false
|
||||||
|
|
||||||
|
|
15
Gemfile.lock
15
Gemfile.lock
|
@ -236,7 +236,7 @@ GEM
|
||||||
devise (>= 4.0.0)
|
devise (>= 4.0.0)
|
||||||
rpam2 (~> 4.0)
|
rpam2 (~> 4.0)
|
||||||
diff-lcs (1.5.0)
|
diff-lcs (1.5.0)
|
||||||
discard (1.2.1)
|
discard (1.3.0)
|
||||||
activerecord (>= 4.2, < 8)
|
activerecord (>= 4.2, < 8)
|
||||||
docile (1.4.0)
|
docile (1.4.0)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
|
@ -265,7 +265,7 @@ GEM
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.100.0)
|
excon (0.100.0)
|
||||||
fabrication (2.30.0)
|
fabrication (2.30.0)
|
||||||
faker (3.2.1)
|
faker (3.2.2)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.3)
|
faraday (1.10.3)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
|
@ -536,7 +536,7 @@ GEM
|
||||||
pundit (2.3.1)
|
pundit (2.3.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.7.1)
|
racc (1.7.3)
|
||||||
rack (2.2.8)
|
rack (2.2.8)
|
||||||
rack-attack (6.7.0)
|
rack-attack (6.7.0)
|
||||||
rack (>= 1.0, < 4)
|
rack (>= 1.0, < 4)
|
||||||
|
@ -650,9 +650,7 @@ GEM
|
||||||
rspec-mocks (~> 3.0)
|
rspec-mocks (~> 3.0)
|
||||||
sidekiq (>= 5, < 8)
|
sidekiq (>= 5, < 8)
|
||||||
rspec-support (3.12.1)
|
rspec-support (3.12.1)
|
||||||
rspec_chunked (0.6)
|
rubocop (1.57.2)
|
||||||
rubocop (1.57.1)
|
|
||||||
base64 (~> 0.1.1)
|
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
|
@ -742,7 +740,7 @@ GEM
|
||||||
stoplight (3.0.2)
|
stoplight (3.0.2)
|
||||||
redlock (~> 1.0)
|
redlock (~> 1.0)
|
||||||
stringio (3.0.8)
|
stringio (3.0.8)
|
||||||
strong_migrations (0.8.0)
|
strong_migrations (1.3.0)
|
||||||
activerecord (>= 5.2)
|
activerecord (>= 5.2)
|
||||||
swd (1.3.0)
|
swd (1.3.0)
|
||||||
activesupport (>= 3)
|
activesupport (>= 3)
|
||||||
|
@ -921,7 +919,6 @@ DEPENDENCIES
|
||||||
rspec-github (~> 2.4)
|
rspec-github (~> 2.4)
|
||||||
rspec-rails (~> 6.0)
|
rspec-rails (~> 6.0)
|
||||||
rspec-sidekiq (~> 4.0)
|
rspec-sidekiq (~> 4.0)
|
||||||
rspec_chunked (~> 0.6)
|
|
||||||
rubocop
|
rubocop
|
||||||
rubocop-capybara
|
rubocop-capybara
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
|
@ -944,7 +941,7 @@ DEPENDENCIES
|
||||||
sprockets-rails (~> 3.4)
|
sprockets-rails (~> 3.4)
|
||||||
stackprof
|
stackprof
|
||||||
stoplight (~> 3.0.1)
|
stoplight (~> 3.0.1)
|
||||||
strong_migrations (~> 0.8)
|
strong_migrations (= 1.3.0)
|
||||||
test-prof
|
test-prof
|
||||||
thor (~> 1.2)
|
thor (~> 1.2)
|
||||||
tty-prompt (~> 0.23)
|
tty-prompt (~> 0.23)
|
||||||
|
|
|
@ -17,6 +17,6 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
||||||
| ------- | ---------------- |
|
| ------- | ---------------- |
|
||||||
| 4.2.x | Yes |
|
| 4.2.x | Yes |
|
||||||
| 4.1.x | Yes |
|
| 4.1.x | Yes |
|
||||||
| 4.0.x | Until 2023-10-31 |
|
| 4.0.x | No |
|
||||||
| 3.5.x | Until 2023-12-31 |
|
| 3.5.x | Until 2023-12-31 |
|
||||||
| < 3.5 | No |
|
| < 3.5 | No |
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { useCallback, useRef, useState, useEffect, forwardRef } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import Textarea from 'react-textarea-autosize';
|
import Textarea from 'react-textarea-autosize';
|
||||||
|
|
||||||
|
@ -37,54 +37,46 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class AutosuggestTextarea extends ImmutablePureComponent {
|
const AutosuggestTextarea = forwardRef(({
|
||||||
|
value,
|
||||||
|
suggestions,
|
||||||
|
disabled,
|
||||||
|
placeholder,
|
||||||
|
onSuggestionSelected,
|
||||||
|
onSuggestionsClearRequested,
|
||||||
|
onSuggestionsFetchRequested,
|
||||||
|
onChange,
|
||||||
|
onKeyUp,
|
||||||
|
onKeyDown,
|
||||||
|
onPaste,
|
||||||
|
onFocus,
|
||||||
|
autoFocus = true,
|
||||||
|
lang,
|
||||||
|
children,
|
||||||
|
}, textareaRef) => {
|
||||||
|
|
||||||
static propTypes = {
|
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
||||||
value: PropTypes.string,
|
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
|
||||||
suggestions: ImmutablePropTypes.list,
|
const lastTokenRef = useRef(null);
|
||||||
disabled: PropTypes.bool,
|
const tokenStartRef = useRef(0);
|
||||||
placeholder: PropTypes.string,
|
|
||||||
onSuggestionSelected: PropTypes.func.isRequired,
|
|
||||||
onSuggestionsClearRequested: PropTypes.func.isRequired,
|
|
||||||
onSuggestionsFetchRequested: PropTypes.func.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onKeyUp: PropTypes.func,
|
|
||||||
onKeyDown: PropTypes.func,
|
|
||||||
onPaste: PropTypes.func.isRequired,
|
|
||||||
autoFocus: PropTypes.bool,
|
|
||||||
lang: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
const handleChange = useCallback((e) => {
|
||||||
autoFocus: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
suggestionsHidden: true,
|
|
||||||
focused: false,
|
|
||||||
selectedSuggestion: 0,
|
|
||||||
lastToken: null,
|
|
||||||
tokenStart: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange = (e) => {
|
|
||||||
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
|
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
|
||||||
|
|
||||||
if (token !== null && this.state.lastToken !== token) {
|
if (token !== null && lastTokenRef.current !== token) {
|
||||||
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
|
tokenStartRef.current = tokenStart;
|
||||||
this.props.onSuggestionsFetchRequested(token);
|
lastTokenRef.current = token;
|
||||||
|
setSelectedSuggestion(0);
|
||||||
|
onSuggestionsFetchRequested(token);
|
||||||
} else if (token === null) {
|
} else if (token === null) {
|
||||||
this.setState({ lastToken: null });
|
lastTokenRef.current = null;
|
||||||
this.props.onSuggestionsClearRequested();
|
onSuggestionsClearRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onChange(e);
|
onChange(e);
|
||||||
};
|
}, [onSuggestionsFetchRequested, onSuggestionsClearRequested, onChange, setSelectedSuggestion]);
|
||||||
|
|
||||||
onKeyDown = (e) => {
|
|
||||||
const { suggestions, disabled } = this.props;
|
|
||||||
const { selectedSuggestion, suggestionsHidden } = this.state;
|
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((e) => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
|
@ -102,80 +94,75 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||||
document.querySelector('.ui').parentElement.focus();
|
document.querySelector('.ui').parentElement.focus();
|
||||||
} else {
|
} else {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.setState({ suggestionsHidden: true });
|
setSuggestionsHidden(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
|
setSelectedSuggestion(Math.min(selectedSuggestion + 1, suggestions.size - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
|
setSelectedSuggestion(Math.max(selectedSuggestion - 1, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
case 'Tab':
|
case 'Tab':
|
||||||
// Select suggestion
|
// Select suggestion
|
||||||
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
|
if (lastTokenRef.current !== null && suggestions.size > 0 && !suggestionsHidden) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
|
onSuggestionSelected(tokenStartRef.current, lastTokenRef.current, suggestions.get(selectedSuggestion));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.defaultPrevented || !this.props.onKeyDown) {
|
if (e.defaultPrevented || !onKeyDown) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onKeyDown(e);
|
onKeyDown(e);
|
||||||
};
|
}, [disabled, suggestions, suggestionsHidden, selectedSuggestion, setSelectedSuggestion, setSuggestionsHidden, onSuggestionSelected, onKeyDown]);
|
||||||
|
|
||||||
onBlur = () => {
|
const handleBlur = useCallback(() => {
|
||||||
this.setState({ suggestionsHidden: true, focused: false });
|
setSuggestionsHidden(true);
|
||||||
};
|
}, [setSuggestionsHidden]);
|
||||||
|
|
||||||
onFocus = (e) => {
|
const handleFocus = useCallback((e) => {
|
||||||
this.setState({ focused: true });
|
if (onFocus) {
|
||||||
if (this.props.onFocus) {
|
onFocus(e);
|
||||||
this.props.onFocus(e);
|
|
||||||
}
|
}
|
||||||
};
|
}, [onFocus]);
|
||||||
|
|
||||||
onSuggestionClick = (e) => {
|
const handleSuggestionClick = useCallback((e) => {
|
||||||
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
|
const suggestion = suggestions.get(e.currentTarget.getAttribute('data-index'));
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
|
onSuggestionSelected(tokenStartRef.current, lastTokenRef.current, suggestion);
|
||||||
this.textarea.focus();
|
textareaRef.current?.focus();
|
||||||
};
|
}, [suggestions, onSuggestionSelected, textareaRef]);
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
const handlePaste = useCallback((e) => {
|
||||||
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
|
|
||||||
this.setState({ suggestionsHidden: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTextarea = (c) => {
|
|
||||||
this.textarea = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
onPaste = (e) => {
|
|
||||||
if (e.clipboardData && e.clipboardData.files.length === 1) {
|
if (e.clipboardData && e.clipboardData.files.length === 1) {
|
||||||
this.props.onPaste(e.clipboardData.files);
|
onPaste(e.clipboardData.files);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
}, [onPaste]);
|
||||||
|
|
||||||
renderSuggestion = (suggestion, i) => {
|
// Show the suggestions again whenever they change and the textarea is focused
|
||||||
const { selectedSuggestion } = this.state;
|
useEffect(() => {
|
||||||
|
if (suggestions.size > 0 && textareaRef.current === document.activeElement) {
|
||||||
|
setSuggestionsHidden(false);
|
||||||
|
}
|
||||||
|
}, [suggestions, textareaRef, setSuggestionsHidden]);
|
||||||
|
|
||||||
|
const renderSuggestion = (suggestion, i) => {
|
||||||
let inner, key;
|
let inner, key;
|
||||||
|
|
||||||
if (suggestion.type === 'emoji') {
|
if (suggestion.type === 'emoji') {
|
||||||
|
@ -190,16 +177,12 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
|
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={handleSuggestionClick}>
|
||||||
{inner}
|
{inner}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
|
||||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, lang, children } = this.props;
|
|
||||||
const { suggestionsHidden } = this.state;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
||||||
<div className='autosuggest-textarea'>
|
<div className='autosuggest-textarea'>
|
||||||
|
@ -207,18 +190,18 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={this.setTextarea}
|
ref={textareaRef}
|
||||||
className='autosuggest-textarea__textarea'
|
className='autosuggest-textarea__textarea'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={this.onChange}
|
onChange={handleChange}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
onFocus={this.onFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={handleBlur}
|
||||||
onPaste={this.onPaste}
|
onPaste={handlePaste}
|
||||||
dir='auto'
|
dir='auto'
|
||||||
aria-autocomplete='list'
|
aria-autocomplete='list'
|
||||||
lang={lang}
|
lang={lang}
|
||||||
|
@ -230,10 +213,28 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
|
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
|
||||||
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
||||||
{suggestions.map(this.renderSuggestion)}
|
{suggestions.map(renderSuggestion)}
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
];
|
];
|
||||||
}
|
});
|
||||||
|
|
||||||
}
|
AutosuggestTextarea.propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
suggestions: ImmutablePropTypes.list,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
onSuggestionSelected: PropTypes.func.isRequired,
|
||||||
|
onSuggestionsClearRequested: PropTypes.func.isRequired,
|
||||||
|
onSuggestionsFetchRequested: PropTypes.func.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onKeyUp: PropTypes.func,
|
||||||
|
onKeyDown: PropTypes.func,
|
||||||
|
onPaste: PropTypes.func.isRequired,
|
||||||
|
onFocus:PropTypes.func,
|
||||||
|
children: PropTypes.node,
|
||||||
|
autoFocus: PropTypes.bool,
|
||||||
|
lang: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AutosuggestTextarea;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { createRef } from 'react';
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
|
@ -90,6 +91,11 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
highlighted: false,
|
highlighted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.textareaRef = createRef(null);
|
||||||
|
}
|
||||||
|
|
||||||
handleChange = (e) => {
|
handleChange = (e) => {
|
||||||
this.props.onChange(e.target.value);
|
this.props.onChange(e.target.value);
|
||||||
};
|
};
|
||||||
|
@ -118,10 +124,10 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
onChangeVisibility,
|
onChangeVisibility,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (this.props.text !== this.textarea.value) {
|
if (this.props.text !== this.textareaRef.current.value) {
|
||||||
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
||||||
// Update the state to match the current text
|
// Update the state to match the current text
|
||||||
this.props.onChange(this.textarea.value);
|
this.props.onChange(this.textareaRef.current.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.canSubmit()) {
|
if (!this.canSubmit()) {
|
||||||
|
@ -154,10 +160,10 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
// Inserts an emoji at the caret.
|
// Inserts an emoji at the caret.
|
||||||
handleEmojiPick = (data) => {
|
handleEmojiPick = (data) => {
|
||||||
const { textarea: { selectionStart } } = this;
|
const position = this.textareaRef.current.selectionStart;
|
||||||
const { onPickEmoji } = this.props;
|
|
||||||
if (onPickEmoji) {
|
if (this.props.onPickEmoji) {
|
||||||
onPickEmoji(selectionStart, data);
|
this.props.onPickEmoji(position, data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -188,13 +194,6 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sets a reference to the textarea.
|
|
||||||
setAutosuggestTextarea = (textareaComponent) => {
|
|
||||||
if (textareaComponent) {
|
|
||||||
this.textarea = textareaComponent.textarea;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sets a reference to the CW field.
|
// Sets a reference to the CW field.
|
||||||
handleRefSpoilerText = (spoilerComponent) => {
|
handleRefSpoilerText = (spoilerComponent) => {
|
||||||
if (spoilerComponent) {
|
if (spoilerComponent) {
|
||||||
|
@ -232,7 +231,6 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
// everyone else from the conversation.
|
// everyone else from the conversation.
|
||||||
_updateFocusAndSelection = (prevProps) => {
|
_updateFocusAndSelection = (prevProps) => {
|
||||||
const {
|
const {
|
||||||
textarea,
|
|
||||||
spoilerText,
|
spoilerText,
|
||||||
} = this;
|
} = this;
|
||||||
const {
|
const {
|
||||||
|
@ -259,30 +257,30 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
default:
|
default:
|
||||||
selectionStart = selectionEnd = text.length;
|
selectionStart = selectionEnd = text.length;
|
||||||
}
|
}
|
||||||
if (textarea) {
|
if (this.textareaRef.current) {
|
||||||
// Because of the wicg-inert polyfill, the activeElement may not be
|
// Because of the wicg-inert polyfill, the activeElement may not be
|
||||||
// immediately selectable, we have to wait for observers to run, as
|
// immediately selectable, we have to wait for observers to run, as
|
||||||
// described in https://github.com/WICG/inert#performance-and-gotchas
|
// described in https://github.com/WICG/inert#performance-and-gotchas
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
textarea.setSelectionRange(selectionStart, selectionEnd);
|
this.textareaRef.current.setSelectionRange(selectionStart, selectionEnd);
|
||||||
textarea.focus();
|
this.textareaRef.current.focus();
|
||||||
if (!singleColumn) textarea.scrollIntoView();
|
if (!singleColumn) this.textareaRef.current.scrollIntoView();
|
||||||
this.setState({ highlighted: true });
|
this.setState({ highlighted: true });
|
||||||
this.timeout = setTimeout(() => this.setState({ highlighted: false }), 700);
|
this.timeout = setTimeout(() => this.setState({ highlighted: false }), 700);
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refocuses the textarea after submitting.
|
// Refocuses the textarea after submitting.
|
||||||
} else if (textarea && prevProps.isSubmitting && !isSubmitting) {
|
} else if (this.textareaRef.current && prevProps.isSubmitting && !isSubmitting) {
|
||||||
textarea.focus();
|
this.textareaRef.current.focus();
|
||||||
} else if (this.props.spoiler !== prevProps.spoiler) {
|
} else if (this.props.spoiler !== prevProps.spoiler) {
|
||||||
if (this.props.spoiler) {
|
if (this.props.spoiler) {
|
||||||
if (spoilerText) {
|
if (spoilerText) {
|
||||||
spoilerText.focus();
|
spoilerText.focus();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (textarea) {
|
if (this.textareaRef.current) {
|
||||||
textarea.focus();
|
this.textareaRef.current.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -347,7 +345,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className={classNames('compose-form__highlightable', { active: highlighted })}>
|
<div className={classNames('compose-form__highlightable', { active: highlighted })}>
|
||||||
<AutosuggestTextarea
|
<AutosuggestTextarea
|
||||||
ref={this.setAutosuggestTextarea}
|
ref={this.textareaRef}
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
value={this.props.text}
|
value={this.props.text}
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import 'core-js/features/object/assign';
|
|
||||||
import 'core-js/features/object/values';
|
|
||||||
import 'core-js/features/symbol';
|
|
||||||
import 'core-js/features/promise/finally';
|
|
||||||
import { decode as decodeBase64 } from '../utils/base64';
|
|
||||||
|
|
||||||
if (!Object.hasOwn(HTMLCanvasElement.prototype, 'toBlob')) {
|
|
||||||
const BASE64_MARKER = ';base64,';
|
|
||||||
|
|
||||||
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
|
|
||||||
value: function (
|
|
||||||
this: HTMLCanvasElement,
|
|
||||||
callback: BlobCallback,
|
|
||||||
type = 'image/png',
|
|
||||||
quality: unknown,
|
|
||||||
) {
|
|
||||||
const dataURL: string = this.toDataURL(type, quality);
|
|
||||||
let data;
|
|
||||||
|
|
||||||
if (dataURL.includes(BASE64_MARKER)) {
|
|
||||||
const [, base64] = dataURL.split(BASE64_MARKER);
|
|
||||||
data = decodeBase64(base64);
|
|
||||||
} else {
|
|
||||||
[, data] = dataURL.split(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(new Blob([data], { type }));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,2 +1 @@
|
||||||
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
|
|
||||||
import 'requestidlecallback';
|
import 'requestidlecallback';
|
||||||
|
|
|
@ -4,39 +4,18 @@
|
||||||
|
|
||||||
import { loadIntlPolyfills } from './intl';
|
import { loadIntlPolyfills } from './intl';
|
||||||
|
|
||||||
function importBasePolyfills() {
|
|
||||||
return import(/* webpackChunkName: "base_polyfills" */ './base_polyfills');
|
|
||||||
}
|
|
||||||
|
|
||||||
function importExtraPolyfills() {
|
function importExtraPolyfills() {
|
||||||
return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills');
|
return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadPolyfills() {
|
export function loadPolyfills() {
|
||||||
const needsBasePolyfills = !(
|
// Safari does not have requestIdleCallback.
|
||||||
'toBlob' in HTMLCanvasElement.prototype &&
|
|
||||||
'assign' in Object &&
|
|
||||||
'values' in Object &&
|
|
||||||
'Symbol' in window &&
|
|
||||||
'finally' in Promise.prototype
|
|
||||||
);
|
|
||||||
|
|
||||||
// Latest version of Firefox and Safari do not have IntersectionObserver.
|
|
||||||
// Edge does not have requestIdleCallback.
|
|
||||||
// This avoids shipping them all the polyfills.
|
// This avoids shipping them all the polyfills.
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types */
|
const needsExtraPolyfills = !window.requestIdleCallback;
|
||||||
const needsExtraPolyfills = !(
|
|
||||||
window.AbortController &&
|
|
||||||
window.IntersectionObserver &&
|
|
||||||
window.IntersectionObserverEntry &&
|
|
||||||
'isIntersecting' in IntersectionObserverEntry.prototype &&
|
|
||||||
window.requestIdleCallback
|
|
||||||
);
|
|
||||||
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
|
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
loadIntlPolyfills(),
|
loadIntlPolyfills(),
|
||||||
needsBasePolyfills && importBasePolyfills(),
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types
|
||||||
needsExtraPolyfills && importExtraPolyfills(),
|
needsExtraPolyfills && importExtraPolyfills(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
|
||||||
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
|
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
|
||||||
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
@ -5,8 +6,7 @@ import api from '../api';
|
||||||
export const submitAccountNote = createAppAsyncThunk(
|
export const submitAccountNote = createAppAsyncThunk(
|
||||||
'account_note/submit',
|
'account_note/submit',
|
||||||
async (args: { id: string; value: string }, { getState }) => {
|
async (args: { id: string; value: string }, { getState }) => {
|
||||||
// TODO: replace `unknown` with `ApiRelationshipJSON` when it is merged
|
const response = await api(getState).post<ApiRelationshipJSON>(
|
||||||
const response = await api(getState).post<unknown>(
|
|
||||||
`/api/v1/accounts/${args.id}/note`,
|
`/api/v1/accounts/${args.id}/note`,
|
||||||
{
|
{
|
||||||
comment: args.value,
|
comment: args.value,
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
|
import {
|
||||||
|
followAccountSuccess, unfollowAccountSuccess,
|
||||||
|
authorizeFollowRequestSuccess, rejectFollowRequestSuccess,
|
||||||
|
followAccountRequest, followAccountFail,
|
||||||
|
unfollowAccountRequest, unfollowAccountFail,
|
||||||
|
muteAccountSuccess, unmuteAccountSuccess,
|
||||||
|
blockAccountSuccess, unblockAccountSuccess,
|
||||||
|
pinAccountSuccess, unpinAccountSuccess,
|
||||||
|
fetchRelationshipsSuccess,
|
||||||
|
} from './accounts_typed';
|
||||||
import { importFetchedAccount, importFetchedAccounts } from './importer';
|
import { importFetchedAccount, importFetchedAccounts } from './importer';
|
||||||
|
|
||||||
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
|
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
|
||||||
|
@ -10,36 +20,22 @@ export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST';
|
||||||
export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
|
export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
|
||||||
export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL';
|
export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL';
|
||||||
|
|
||||||
export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
|
|
||||||
export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
|
|
||||||
export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL';
|
|
||||||
|
|
||||||
export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST';
|
|
||||||
export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS';
|
|
||||||
export const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL';
|
|
||||||
|
|
||||||
export const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST';
|
export const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST';
|
||||||
export const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS';
|
|
||||||
export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL';
|
export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL';
|
||||||
|
|
||||||
export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST';
|
export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST';
|
||||||
export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS';
|
|
||||||
export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL';
|
export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL';
|
||||||
|
|
||||||
export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST';
|
export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST';
|
||||||
export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS';
|
|
||||||
export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL';
|
export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL';
|
||||||
|
|
||||||
export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST';
|
export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST';
|
||||||
export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS';
|
|
||||||
export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL';
|
export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL';
|
||||||
|
|
||||||
export const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST';
|
export const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST';
|
||||||
export const ACCOUNT_PIN_SUCCESS = 'ACCOUNT_PIN_SUCCESS';
|
|
||||||
export const ACCOUNT_PIN_FAIL = 'ACCOUNT_PIN_FAIL';
|
export const ACCOUNT_PIN_FAIL = 'ACCOUNT_PIN_FAIL';
|
||||||
|
|
||||||
export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST';
|
export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST';
|
||||||
export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS';
|
|
||||||
export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL';
|
export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL';
|
||||||
|
|
||||||
export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
|
export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
|
||||||
|
@ -59,7 +55,6 @@ export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS';
|
||||||
export const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL';
|
export const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL';
|
||||||
|
|
||||||
export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST';
|
export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST';
|
||||||
export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS';
|
|
||||||
export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL';
|
export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL';
|
||||||
|
|
||||||
export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST';
|
export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST';
|
||||||
|
@ -71,15 +66,15 @@ export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS';
|
||||||
export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
|
export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
|
||||||
|
|
||||||
export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
|
export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
|
||||||
export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS';
|
|
||||||
export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
|
export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
|
||||||
|
|
||||||
export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
|
export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
|
||||||
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
|
|
||||||
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
|
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
|
||||||
|
|
||||||
export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL';
|
export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL';
|
||||||
|
|
||||||
|
export * from './accounts_typed';
|
||||||
|
|
||||||
export function fetchAccount(id) {
|
export function fetchAccount(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchRelationships([id]));
|
dispatch(fetchRelationships([id]));
|
||||||
|
@ -149,12 +144,12 @@ export function followAccount(id, options = { reblogs: true }) {
|
||||||
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
||||||
const locked = getState().getIn(['accounts', id, 'locked'], false);
|
const locked = getState().getIn(['accounts', id, 'locked'], false);
|
||||||
|
|
||||||
dispatch(followAccountRequest(id, locked));
|
dispatch(followAccountRequest({ id, locked }));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
|
api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
|
||||||
dispatch(followAccountSuccess(response.data, alreadyFollowing));
|
dispatch(followAccountSuccess({relationship: response.data, alreadyFollowing}));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(followAccountFail(error, locked));
|
dispatch(followAccountFail({ id, error, locked }));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -164,74 +159,22 @@ export function unfollowAccount(id) {
|
||||||
dispatch(unfollowAccountRequest(id));
|
dispatch(unfollowAccountRequest(id));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => {
|
api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => {
|
||||||
dispatch(unfollowAccountSuccess(response.data, getState().get('statuses')));
|
dispatch(unfollowAccountSuccess({relationship: response.data, statuses: getState().get('statuses')}));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(unfollowAccountFail(error));
|
dispatch(unfollowAccountFail({ id, error }));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function followAccountRequest(id, locked) {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_FOLLOW_REQUEST,
|
|
||||||
id,
|
|
||||||
locked,
|
|
||||||
skipLoading: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function followAccountSuccess(relationship, alreadyFollowing) {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_FOLLOW_SUCCESS,
|
|
||||||
relationship,
|
|
||||||
alreadyFollowing,
|
|
||||||
skipLoading: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function followAccountFail(error, locked) {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_FOLLOW_FAIL,
|
|
||||||
error,
|
|
||||||
locked,
|
|
||||||
skipLoading: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unfollowAccountRequest(id) {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_UNFOLLOW_REQUEST,
|
|
||||||
id,
|
|
||||||
skipLoading: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unfollowAccountSuccess(relationship, statuses) {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_UNFOLLOW_SUCCESS,
|
|
||||||
relationship,
|
|
||||||
statuses,
|
|
||||||
skipLoading: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unfollowAccountFail(error) {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_UNFOLLOW_FAIL,
|
|
||||||
error,
|
|
||||||
skipLoading: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function blockAccount(id) {
|
export function blockAccount(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(blockAccountRequest(id));
|
dispatch(blockAccountRequest(id));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/block`).then(response => {
|
api(getState).post(`/api/v1/accounts/${id}/block`).then(response => {
|
||||||
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
|
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
|
||||||
dispatch(blockAccountSuccess(response.data, getState().get('statuses')));
|
dispatch(blockAccountSuccess({ relationship: response.data, statuses: getState().get('statuses') }));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(blockAccountFail(id, error));
|
dispatch(blockAccountFail({ id, error }));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -241,9 +184,9 @@ export function unblockAccount(id) {
|
||||||
dispatch(unblockAccountRequest(id));
|
dispatch(unblockAccountRequest(id));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => {
|
api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => {
|
||||||
dispatch(unblockAccountSuccess(response.data));
|
dispatch(unblockAccountSuccess({ relationship: response.data }));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(unblockAccountFail(id, error));
|
dispatch(unblockAccountFail({ id, error }));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -254,15 +197,6 @@ export function blockAccountRequest(id) {
|
||||||
id,
|
id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function blockAccountSuccess(relationship, statuses) {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_BLOCK_SUCCESS,
|
|
||||||
relationship,
|
|
||||||
statuses,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function blockAccountFail(error) {
|
export function blockAccountFail(error) {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_BLOCK_FAIL,
|
type: ACCOUNT_BLOCK_FAIL,
|
||||||
|
@ -277,13 +211,6 @@ export function unblockAccountRequest(id) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unblockAccountSuccess(relationship) {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_UNBLOCK_SUCCESS,
|
|
||||||
relationship,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unblockAccountFail(error) {
|
export function unblockAccountFail(error) {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_UNBLOCK_FAIL,
|
type: ACCOUNT_UNBLOCK_FAIL,
|
||||||
|
@ -298,9 +225,9 @@ export function muteAccount(id, notifications, duration=0) {
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => {
|
api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => {
|
||||||
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
|
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
|
||||||
dispatch(muteAccountSuccess(response.data, getState().get('statuses')));
|
dispatch(muteAccountSuccess({ relationship: response.data, statuses: getState().get('statuses') }));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(muteAccountFail(id, error));
|
dispatch(muteAccountFail({ id, error }));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -310,9 +237,9 @@ export function unmuteAccount(id) {
|
||||||
dispatch(unmuteAccountRequest(id));
|
dispatch(unmuteAccountRequest(id));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => {
|
api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => {
|
||||||
dispatch(unmuteAccountSuccess(response.data));
|
dispatch(unmuteAccountSuccess({ relationship: response.data }));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(unmuteAccountFail(id, error));
|
dispatch(unmuteAccountFail({ id, error }));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -324,14 +251,6 @@ export function muteAccountRequest(id) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function muteAccountSuccess(relationship, statuses) {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_MUTE_SUCCESS,
|
|
||||||
relationship,
|
|
||||||
statuses,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function muteAccountFail(error) {
|
export function muteAccountFail(error) {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_MUTE_FAIL,
|
type: ACCOUNT_MUTE_FAIL,
|
||||||
|
@ -346,13 +265,6 @@ export function unmuteAccountRequest(id) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unmuteAccountSuccess(relationship) {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_UNMUTE_SUCCESS,
|
|
||||||
relationship,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unmuteAccountFail(error) {
|
export function unmuteAccountFail(error) {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_UNMUTE_FAIL,
|
type: ACCOUNT_UNMUTE_FAIL,
|
||||||
|
@ -549,7 +461,7 @@ export function fetchRelationships(accountIds) {
|
||||||
dispatch(fetchRelationshipsRequest(newAccountIds));
|
dispatch(fetchRelationshipsRequest(newAccountIds));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
|
api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
|
||||||
dispatch(fetchRelationshipsSuccess(response.data));
|
dispatch(fetchRelationshipsSuccess({ relationships: response.data }));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchRelationshipsFail(error));
|
dispatch(fetchRelationshipsFail(error));
|
||||||
});
|
});
|
||||||
|
@ -564,14 +476,6 @@ export function fetchRelationshipsRequest(ids) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchRelationshipsSuccess(relationships) {
|
|
||||||
return {
|
|
||||||
type: RELATIONSHIPS_FETCH_SUCCESS,
|
|
||||||
relationships,
|
|
||||||
skipLoading: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchRelationshipsFail(error) {
|
export function fetchRelationshipsFail(error) {
|
||||||
return {
|
return {
|
||||||
type: RELATIONSHIPS_FETCH_FAIL,
|
type: RELATIONSHIPS_FETCH_FAIL,
|
||||||
|
@ -659,7 +563,7 @@ export function authorizeFollowRequest(id) {
|
||||||
|
|
||||||
api(getState)
|
api(getState)
|
||||||
.post(`/api/v1/follow_requests/${id}/authorize`)
|
.post(`/api/v1/follow_requests/${id}/authorize`)
|
||||||
.then(() => dispatch(authorizeFollowRequestSuccess(id)))
|
.then(() => dispatch(authorizeFollowRequestSuccess({ id })))
|
||||||
.catch(error => dispatch(authorizeFollowRequestFail(id, error)));
|
.catch(error => dispatch(authorizeFollowRequestFail(id, error)));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -671,13 +575,6 @@ export function authorizeFollowRequestRequest(id) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function authorizeFollowRequestSuccess(id) {
|
|
||||||
return {
|
|
||||||
type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
|
|
||||||
id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function authorizeFollowRequestFail(id, error) {
|
export function authorizeFollowRequestFail(id, error) {
|
||||||
return {
|
return {
|
||||||
type: FOLLOW_REQUEST_AUTHORIZE_FAIL,
|
type: FOLLOW_REQUEST_AUTHORIZE_FAIL,
|
||||||
|
@ -693,7 +590,7 @@ export function rejectFollowRequest(id) {
|
||||||
|
|
||||||
api(getState)
|
api(getState)
|
||||||
.post(`/api/v1/follow_requests/${id}/reject`)
|
.post(`/api/v1/follow_requests/${id}/reject`)
|
||||||
.then(() => dispatch(rejectFollowRequestSuccess(id)))
|
.then(() => dispatch(rejectFollowRequestSuccess({ id })))
|
||||||
.catch(error => dispatch(rejectFollowRequestFail(id, error)));
|
.catch(error => dispatch(rejectFollowRequestFail(id, error)));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -705,13 +602,6 @@ export function rejectFollowRequestRequest(id) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rejectFollowRequestSuccess(id) {
|
|
||||||
return {
|
|
||||||
type: FOLLOW_REQUEST_REJECT_SUCCESS,
|
|
||||||
id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function rejectFollowRequestFail(id, error) {
|
export function rejectFollowRequestFail(id, error) {
|
||||||
return {
|
return {
|
||||||
type: FOLLOW_REQUEST_REJECT_FAIL,
|
type: FOLLOW_REQUEST_REJECT_FAIL,
|
||||||
|
@ -725,7 +615,7 @@ export function pinAccount(id) {
|
||||||
dispatch(pinAccountRequest(id));
|
dispatch(pinAccountRequest(id));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => {
|
api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => {
|
||||||
dispatch(pinAccountSuccess(response.data));
|
dispatch(pinAccountSuccess({ relationship: response.data }));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(pinAccountFail(error));
|
dispatch(pinAccountFail(error));
|
||||||
});
|
});
|
||||||
|
@ -737,7 +627,7 @@ export function unpinAccount(id) {
|
||||||
dispatch(unpinAccountRequest(id));
|
dispatch(unpinAccountRequest(id));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => {
|
api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => {
|
||||||
dispatch(unpinAccountSuccess(response.data));
|
dispatch(unpinAccountSuccess({ relationship: response.data }));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(unpinAccountFail(error));
|
dispatch(unpinAccountFail(error));
|
||||||
});
|
});
|
||||||
|
@ -751,13 +641,6 @@ export function pinAccountRequest(id) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pinAccountSuccess(relationship) {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_PIN_SUCCESS,
|
|
||||||
relationship,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pinAccountFail(error) {
|
export function pinAccountFail(error) {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_PIN_FAIL,
|
type: ACCOUNT_PIN_FAIL,
|
||||||
|
@ -772,21 +655,9 @@ export function unpinAccountRequest(id) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unpinAccountSuccess(relationship) {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_UNPIN_SUCCESS,
|
|
||||||
relationship,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unpinAccountFail(error) {
|
export function unpinAccountFail(error) {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_UNPIN_FAIL,
|
type: ACCOUNT_UNPIN_FAIL,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const revealAccount = id => ({
|
|
||||||
type: ACCOUNT_REVEAL,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
97
app/javascript/mastodon/actions/accounts_typed.ts
Normal file
97
app/javascript/mastodon/actions/accounts_typed.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||||
|
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
|
||||||
|
|
||||||
|
export const revealAccount = createAction<{
|
||||||
|
id: string;
|
||||||
|
}>('accounts/revealAccount');
|
||||||
|
|
||||||
|
export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>(
|
||||||
|
'accounts/importAccounts',
|
||||||
|
);
|
||||||
|
|
||||||
|
function actionWithSkipLoadingTrue<Args extends object>(args: Args) {
|
||||||
|
return {
|
||||||
|
payload: {
|
||||||
|
...args,
|
||||||
|
skipLoading: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const followAccountSuccess = createAction(
|
||||||
|
'accounts/followAccountSuccess',
|
||||||
|
actionWithSkipLoadingTrue<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
alreadyFollowing: boolean;
|
||||||
|
}>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const unfollowAccountSuccess = createAction(
|
||||||
|
'accounts/unfollowAccountSuccess',
|
||||||
|
actionWithSkipLoadingTrue<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
statuses: unknown;
|
||||||
|
alreadyFollowing?: boolean;
|
||||||
|
}>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const authorizeFollowRequestSuccess = createAction<{ id: string }>(
|
||||||
|
'accounts/followRequestAuthorizeSuccess',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const rejectFollowRequestSuccess = createAction<{ id: string }>(
|
||||||
|
'accounts/followRequestRejectSuccess',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const followAccountRequest = createAction(
|
||||||
|
'accounts/followRequest',
|
||||||
|
actionWithSkipLoadingTrue<{ id: string; locked: boolean }>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const followAccountFail = createAction(
|
||||||
|
'accounts/followFail',
|
||||||
|
actionWithSkipLoadingTrue<{ id: string; error: string; locked: boolean }>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const unfollowAccountRequest = createAction(
|
||||||
|
'accounts/unfollowRequest',
|
||||||
|
actionWithSkipLoadingTrue<{ id: string }>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const unfollowAccountFail = createAction(
|
||||||
|
'accounts/unfollowFail',
|
||||||
|
actionWithSkipLoadingTrue<{ id: string; error: string }>,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const blockAccountSuccess = createAction<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
statuses: unknown;
|
||||||
|
}>('accounts/blockSuccess');
|
||||||
|
|
||||||
|
export const unblockAccountSuccess = createAction<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
}>('accounts/unblockSuccess');
|
||||||
|
|
||||||
|
export const muteAccountSuccess = createAction<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
statuses: unknown;
|
||||||
|
}>('accounts/muteSuccess');
|
||||||
|
|
||||||
|
export const unmuteAccountSuccess = createAction<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
}>('accounts/unmuteSuccess');
|
||||||
|
|
||||||
|
export const pinAccountSuccess = createAction<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
}>('accounts/pinSuccess');
|
||||||
|
|
||||||
|
export const unpinAccountSuccess = createAction<{
|
||||||
|
relationship: ApiRelationshipJSON;
|
||||||
|
}>('accounts/unpinSuccess');
|
||||||
|
|
||||||
|
export const fetchRelationshipsSuccess = createAction(
|
||||||
|
'relationships/fetchSuccess',
|
||||||
|
actionWithSkipLoadingTrue<{ relationships: ApiRelationshipJSON[] }>,
|
||||||
|
);
|
|
@ -1,11 +1,13 @@
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
|
import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed";
|
||||||
|
|
||||||
|
export * from "./domain_blocks_typed";
|
||||||
|
|
||||||
export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
|
export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
|
||||||
export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS';
|
|
||||||
export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL';
|
export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL';
|
||||||
|
|
||||||
export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST';
|
export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST';
|
||||||
export const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS';
|
|
||||||
export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL';
|
export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL';
|
||||||
|
|
||||||
export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST';
|
export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST';
|
||||||
|
@ -24,7 +26,7 @@ export function blockDomain(domain) {
|
||||||
const at_domain = '@' + domain;
|
const at_domain = '@' + domain;
|
||||||
const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
|
const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
|
||||||
|
|
||||||
dispatch(blockDomainSuccess(domain, accounts));
|
dispatch(blockDomainSuccess({ domain, accounts }));
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
dispatch(blockDomainFail(domain, err));
|
dispatch(blockDomainFail(domain, err));
|
||||||
});
|
});
|
||||||
|
@ -38,14 +40,6 @@ export function blockDomainRequest(domain) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function blockDomainSuccess(domain, accounts) {
|
|
||||||
return {
|
|
||||||
type: DOMAIN_BLOCK_SUCCESS,
|
|
||||||
domain,
|
|
||||||
accounts,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function blockDomainFail(domain, error) {
|
export function blockDomainFail(domain, error) {
|
||||||
return {
|
return {
|
||||||
type: DOMAIN_BLOCK_FAIL,
|
type: DOMAIN_BLOCK_FAIL,
|
||||||
|
@ -61,7 +55,7 @@ export function unblockDomain(domain) {
|
||||||
api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => {
|
api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => {
|
||||||
const at_domain = '@' + domain;
|
const at_domain = '@' + domain;
|
||||||
const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
|
const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id'));
|
||||||
dispatch(unblockDomainSuccess(domain, accounts));
|
dispatch(unblockDomainSuccess({ domain, accounts }));
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
dispatch(unblockDomainFail(domain, err));
|
dispatch(unblockDomainFail(domain, err));
|
||||||
});
|
});
|
||||||
|
@ -75,14 +69,6 @@ export function unblockDomainRequest(domain) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unblockDomainSuccess(domain, accounts) {
|
|
||||||
return {
|
|
||||||
type: DOMAIN_UNBLOCK_SUCCESS,
|
|
||||||
domain,
|
|
||||||
accounts,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unblockDomainFail(domain, error) {
|
export function unblockDomainFail(domain, error) {
|
||||||
return {
|
return {
|
||||||
type: DOMAIN_UNBLOCK_FAIL,
|
type: DOMAIN_UNBLOCK_FAIL,
|
||||||
|
|
13
app/javascript/mastodon/actions/domain_blocks_typed.ts
Normal file
13
app/javascript/mastodon/actions/domain_blocks_typed.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
|
||||||
|
export const blockDomainSuccess = createAction<{
|
||||||
|
domain: string;
|
||||||
|
accounts: Account[];
|
||||||
|
}>('domain_blocks/blockSuccess');
|
||||||
|
|
||||||
|
export const unblockDomainSuccess = createAction<{
|
||||||
|
domain: string;
|
||||||
|
accounts: Account[];
|
||||||
|
}>('domain_blocks/unblockSuccess');
|
|
@ -1,7 +1,7 @@
|
||||||
import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer';
|
import { importAccounts } from '../accounts_typed';
|
||||||
|
|
||||||
|
import { normalizeStatus, normalizePoll } from './normalizer';
|
||||||
|
|
||||||
export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
|
|
||||||
export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
|
|
||||||
export const STATUS_IMPORT = 'STATUS_IMPORT';
|
export const STATUS_IMPORT = 'STATUS_IMPORT';
|
||||||
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
|
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
|
||||||
export const POLLS_IMPORT = 'POLLS_IMPORT';
|
export const POLLS_IMPORT = 'POLLS_IMPORT';
|
||||||
|
@ -13,14 +13,6 @@ function pushUnique(array, object) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function importAccount(account) {
|
|
||||||
return { type: ACCOUNT_IMPORT, account };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function importAccounts(accounts) {
|
|
||||||
return { type: ACCOUNTS_IMPORT, accounts };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function importStatus(status) {
|
export function importStatus(status) {
|
||||||
return { type: STATUS_IMPORT, status };
|
return { type: STATUS_IMPORT, status };
|
||||||
}
|
}
|
||||||
|
@ -45,7 +37,7 @@ export function importFetchedAccounts(accounts) {
|
||||||
const normalAccounts = [];
|
const normalAccounts = [];
|
||||||
|
|
||||||
function processAccount(account) {
|
function processAccount(account) {
|
||||||
pushUnique(normalAccounts, normalizeAccount(account));
|
pushUnique(normalAccounts, account);
|
||||||
|
|
||||||
if (account.moved) {
|
if (account.moved) {
|
||||||
processAccount(account.moved);
|
processAccount(account.moved);
|
||||||
|
@ -54,7 +46,7 @@ export function importFetchedAccounts(accounts) {
|
||||||
|
|
||||||
accounts.forEach(processAccount);
|
accounts.forEach(processAccount);
|
||||||
|
|
||||||
return importAccounts(normalAccounts);
|
return importAccounts({ accounts: normalAccounts });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function importFetchedStatus(status) {
|
export function importFetchedStatus(status) {
|
||||||
|
|
|
@ -2,7 +2,6 @@ import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
|
||||||
import emojify from '../../features/emoji/emoji';
|
import emojify from '../../features/emoji/emoji';
|
||||||
import { expandSpoilers } from '../../initial_state';
|
import { expandSpoilers } from '../../initial_state';
|
||||||
import { unescapeHTML } from '../../utils/html';
|
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
|
@ -17,32 +16,6 @@ export function searchTextFromRawStatus (status) {
|
||||||
return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeAccount(account) {
|
|
||||||
account = { ...account };
|
|
||||||
|
|
||||||
const emojiMap = makeEmojiMap(account.emojis);
|
|
||||||
const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;
|
|
||||||
|
|
||||||
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
|
|
||||||
account.note_emojified = emojify(account.note, emojiMap);
|
|
||||||
account.note_plain = unescapeHTML(account.note);
|
|
||||||
|
|
||||||
if (account.fields) {
|
|
||||||
account.fields = account.fields.map(pair => ({
|
|
||||||
...pair,
|
|
||||||
name_emojified: emojify(escapeTextContentForBrowser(pair.name), emojiMap),
|
|
||||||
value_emojified: emojify(pair.value, emojiMap),
|
|
||||||
value_plain: unescapeHTML(pair.value),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.moved) {
|
|
||||||
account.moved = account.moved.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeFilterResult(result) {
|
export function normalizeFilterResult(result) {
|
||||||
const normalResult = { ...result };
|
const normalResult = { ...result };
|
||||||
|
|
||||||
|
|
|
@ -18,10 +18,12 @@ import {
|
||||||
importFetchedStatuses,
|
importFetchedStatuses,
|
||||||
} from './importer';
|
} from './importer';
|
||||||
import { submitMarkers } from './markers';
|
import { submitMarkers } from './markers';
|
||||||
|
import { notificationsUpdate } from "./notifications_typed";
|
||||||
import { register as registerPushNotifications } from './push_notifications';
|
import { register as registerPushNotifications } from './push_notifications';
|
||||||
import { saveSettings } from './settings';
|
import { saveSettings } from './settings';
|
||||||
|
|
||||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
export * from "./notifications_typed";
|
||||||
|
|
||||||
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
||||||
|
|
||||||
export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
|
export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
|
||||||
|
@ -95,12 +97,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
dispatch(importFetchedAccount(notification.report.target_account));
|
dispatch(importFetchedAccount(notification.report.target_account));
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: NOTIFICATIONS_UPDATE,
|
dispatch(notificationsUpdate(notification, preferPendingItems, playSound && !filtered));
|
||||||
notification,
|
|
||||||
usePendingItems: preferPendingItems,
|
|
||||||
meta: (playSound && !filtered) ? { sound: 'boop' } : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchRelatedRelationships(dispatch, [notification]);
|
fetchRelatedRelationships(dispatch, [notification]);
|
||||||
} else if (playSound && !filtered) {
|
} else if (playSound && !filtered) {
|
||||||
|
|
23
app/javascript/mastodon/actions/notifications_typed.ts
Normal file
23
app/javascript/mastodon/actions/notifications_typed.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import type { ApiAccountJSON } from '../api_types/accounts';
|
||||||
|
// To be replaced once ApiNotificationJSON type exists
|
||||||
|
interface FakeApiNotificationJSON {
|
||||||
|
type: string;
|
||||||
|
account: ApiAccountJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const notificationsUpdate = createAction(
|
||||||
|
'notifications/update',
|
||||||
|
({
|
||||||
|
playSound,
|
||||||
|
...args
|
||||||
|
}: {
|
||||||
|
notification: FakeApiNotificationJSON;
|
||||||
|
usePendingItems: boolean;
|
||||||
|
playSound: boolean;
|
||||||
|
}) => ({
|
||||||
|
payload: args,
|
||||||
|
meta: { playSound: playSound ? { sound: 'boop' } : undefined },
|
||||||
|
}),
|
||||||
|
);
|
|
@ -11,6 +11,7 @@ const convertState = rawState =>
|
||||||
fromJS(rawState, (k, v) =>
|
fromJS(rawState, (k, v) =>
|
||||||
Iterable.isIndexed(v) ? v.toList() : v.toMap());
|
Iterable.isIndexed(v) ? v.toList() : v.toMap());
|
||||||
|
|
||||||
|
|
||||||
export function hydrateStore(rawState) {
|
export function hydrateStore(rawState) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
const state = convertState(rawState);
|
const state = convertState(rawState);
|
||||||
|
|
|
@ -31,9 +31,9 @@ export interface ApiAccountJSON {
|
||||||
id: string;
|
id: string;
|
||||||
last_status_at: string;
|
last_status_at: string;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
noindex: boolean;
|
noindex?: boolean;
|
||||||
note: string;
|
note: string;
|
||||||
roles: ApiAccountJSON[];
|
roles?: ApiAccountJSON[];
|
||||||
statuses_count: number;
|
statuses_count: number;
|
||||||
uri: string;
|
uri: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
|
|
@ -36,7 +36,7 @@ class Account extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.record,
|
||||||
onFollow: PropTypes.func.isRequired,
|
onFollow: PropTypes.func.isRequired,
|
||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func.isRequired,
|
||||||
onMute: PropTypes.func.isRequired,
|
onMute: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { useCallback, useRef, useState, useEffect, forwardRef } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import Textarea from 'react-textarea-autosize';
|
import Textarea from 'react-textarea-autosize';
|
||||||
|
|
||||||
|
@ -37,54 +37,46 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class AutosuggestTextarea extends ImmutablePureComponent {
|
const AutosuggestTextarea = forwardRef(({
|
||||||
|
value,
|
||||||
|
suggestions,
|
||||||
|
disabled,
|
||||||
|
placeholder,
|
||||||
|
onSuggestionSelected,
|
||||||
|
onSuggestionsClearRequested,
|
||||||
|
onSuggestionsFetchRequested,
|
||||||
|
onChange,
|
||||||
|
onKeyUp,
|
||||||
|
onKeyDown,
|
||||||
|
onPaste,
|
||||||
|
onFocus,
|
||||||
|
autoFocus = true,
|
||||||
|
lang,
|
||||||
|
children,
|
||||||
|
}, textareaRef) => {
|
||||||
|
|
||||||
static propTypes = {
|
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
||||||
value: PropTypes.string,
|
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
|
||||||
suggestions: ImmutablePropTypes.list,
|
const lastTokenRef = useRef(null);
|
||||||
disabled: PropTypes.bool,
|
const tokenStartRef = useRef(0);
|
||||||
placeholder: PropTypes.string,
|
|
||||||
onSuggestionSelected: PropTypes.func.isRequired,
|
|
||||||
onSuggestionsClearRequested: PropTypes.func.isRequired,
|
|
||||||
onSuggestionsFetchRequested: PropTypes.func.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onKeyUp: PropTypes.func,
|
|
||||||
onKeyDown: PropTypes.func,
|
|
||||||
onPaste: PropTypes.func.isRequired,
|
|
||||||
autoFocus: PropTypes.bool,
|
|
||||||
lang: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
const handleChange = useCallback((e) => {
|
||||||
autoFocus: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
suggestionsHidden: true,
|
|
||||||
focused: false,
|
|
||||||
selectedSuggestion: 0,
|
|
||||||
lastToken: null,
|
|
||||||
tokenStart: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange = (e) => {
|
|
||||||
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
|
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
|
||||||
|
|
||||||
if (token !== null && this.state.lastToken !== token) {
|
if (token !== null && lastTokenRef.current !== token) {
|
||||||
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
|
tokenStartRef.current = tokenStart;
|
||||||
this.props.onSuggestionsFetchRequested(token);
|
lastTokenRef.current = token;
|
||||||
|
setSelectedSuggestion(0);
|
||||||
|
onSuggestionsFetchRequested(token);
|
||||||
} else if (token === null) {
|
} else if (token === null) {
|
||||||
this.setState({ lastToken: null });
|
lastTokenRef.current = null;
|
||||||
this.props.onSuggestionsClearRequested();
|
onSuggestionsClearRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onChange(e);
|
onChange(e);
|
||||||
};
|
}, [onSuggestionsFetchRequested, onSuggestionsClearRequested, onChange, setSelectedSuggestion]);
|
||||||
|
|
||||||
onKeyDown = (e) => {
|
|
||||||
const { suggestions, disabled } = this.props;
|
|
||||||
const { selectedSuggestion, suggestionsHidden } = this.state;
|
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((e) => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
|
@ -102,80 +94,75 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||||
document.querySelector('.ui').parentElement.focus();
|
document.querySelector('.ui').parentElement.focus();
|
||||||
} else {
|
} else {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.setState({ suggestionsHidden: true });
|
setSuggestionsHidden(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
|
setSelectedSuggestion(Math.min(selectedSuggestion + 1, suggestions.size - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
|
setSelectedSuggestion(Math.max(selectedSuggestion - 1, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
case 'Tab':
|
case 'Tab':
|
||||||
// Select suggestion
|
// Select suggestion
|
||||||
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
|
if (lastTokenRef.current !== null && suggestions.size > 0 && !suggestionsHidden) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
|
onSuggestionSelected(tokenStartRef.current, lastTokenRef.current, suggestions.get(selectedSuggestion));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.defaultPrevented || !this.props.onKeyDown) {
|
if (e.defaultPrevented || !onKeyDown) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onKeyDown(e);
|
onKeyDown(e);
|
||||||
};
|
}, [disabled, suggestions, suggestionsHidden, selectedSuggestion, setSelectedSuggestion, setSuggestionsHidden, onSuggestionSelected, onKeyDown]);
|
||||||
|
|
||||||
onBlur = () => {
|
const handleBlur = useCallback(() => {
|
||||||
this.setState({ suggestionsHidden: true, focused: false });
|
setSuggestionsHidden(true);
|
||||||
};
|
}, [setSuggestionsHidden]);
|
||||||
|
|
||||||
onFocus = (e) => {
|
const handleFocus = useCallback((e) => {
|
||||||
this.setState({ focused: true });
|
if (onFocus) {
|
||||||
if (this.props.onFocus) {
|
onFocus(e);
|
||||||
this.props.onFocus(e);
|
|
||||||
}
|
}
|
||||||
};
|
}, [onFocus]);
|
||||||
|
|
||||||
onSuggestionClick = (e) => {
|
const handleSuggestionClick = useCallback((e) => {
|
||||||
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
|
const suggestion = suggestions.get(e.currentTarget.getAttribute('data-index'));
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
|
onSuggestionSelected(tokenStartRef.current, lastTokenRef.current, suggestion);
|
||||||
this.textarea.focus();
|
textareaRef.current?.focus();
|
||||||
};
|
}, [suggestions, onSuggestionSelected, textareaRef]);
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
const handlePaste = useCallback((e) => {
|
||||||
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
|
|
||||||
this.setState({ suggestionsHidden: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTextarea = (c) => {
|
|
||||||
this.textarea = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
onPaste = (e) => {
|
|
||||||
if (e.clipboardData && e.clipboardData.files.length === 1) {
|
if (e.clipboardData && e.clipboardData.files.length === 1) {
|
||||||
this.props.onPaste(e.clipboardData.files);
|
onPaste(e.clipboardData.files);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
}, [onPaste]);
|
||||||
|
|
||||||
renderSuggestion = (suggestion, i) => {
|
// Show the suggestions again whenever they change and the textarea is focused
|
||||||
const { selectedSuggestion } = this.state;
|
useEffect(() => {
|
||||||
|
if (suggestions.size > 0 && textareaRef.current === document.activeElement) {
|
||||||
|
setSuggestionsHidden(false);
|
||||||
|
}
|
||||||
|
}, [suggestions, textareaRef, setSuggestionsHidden]);
|
||||||
|
|
||||||
|
const renderSuggestion = (suggestion, i) => {
|
||||||
let inner, key;
|
let inner, key;
|
||||||
|
|
||||||
if (suggestion.type === 'emoji') {
|
if (suggestion.type === 'emoji') {
|
||||||
|
@ -190,16 +177,12 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
|
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={handleSuggestionClick}>
|
||||||
{inner}
|
{inner}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
|
||||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, lang, children } = this.props;
|
|
||||||
const { suggestionsHidden } = this.state;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
||||||
<div className='autosuggest-textarea'>
|
<div className='autosuggest-textarea'>
|
||||||
|
@ -207,18 +190,18 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={this.setTextarea}
|
ref={textareaRef}
|
||||||
className='autosuggest-textarea__textarea'
|
className='autosuggest-textarea__textarea'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={this.onChange}
|
onChange={handleChange}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
onFocus={this.onFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={handleBlur}
|
||||||
onPaste={this.onPaste}
|
onPaste={handlePaste}
|
||||||
dir='auto'
|
dir='auto'
|
||||||
aria-autocomplete='list'
|
aria-autocomplete='list'
|
||||||
lang={lang}
|
lang={lang}
|
||||||
|
@ -230,10 +213,28 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
|
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
|
||||||
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
||||||
{suggestions.map(this.renderSuggestion)}
|
{suggestions.map(renderSuggestion)}
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
];
|
];
|
||||||
}
|
});
|
||||||
|
|
||||||
}
|
AutosuggestTextarea.propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
suggestions: ImmutablePropTypes.list,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
onSuggestionSelected: PropTypes.func.isRequired,
|
||||||
|
onSuggestionsClearRequested: PropTypes.func.isRequired,
|
||||||
|
onSuggestionsFetchRequested: PropTypes.func.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onKeyUp: PropTypes.func,
|
||||||
|
onKeyDown: PropTypes.func,
|
||||||
|
onPaste: PropTypes.func.isRequired,
|
||||||
|
onFocus:PropTypes.func,
|
||||||
|
children: PropTypes.node,
|
||||||
|
autoFocus: PropTypes.bool,
|
||||||
|
lang: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AutosuggestTextarea;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
|
||||||
import { useHovering } from '../../hooks/useHovering';
|
import { useHovering } from '../../hooks/useHovering';
|
||||||
import type { Account } from '../../types/resources';
|
|
||||||
import { autoPlayGif } from '../initial_state';
|
import { autoPlayGif } from '../initial_state';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
|
||||||
import { useHovering } from '../../hooks/useHovering';
|
import { useHovering } from '../../hooks/useHovering';
|
||||||
import type { Account } from '../../types/resources';
|
|
||||||
import { autoPlayGif } from '../initial_state';
|
import { autoPlayGif } from '../initial_state';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -2,7 +2,8 @@ import React from 'react';
|
||||||
|
|
||||||
import type { List } from 'immutable';
|
import type { List } from 'immutable';
|
||||||
|
|
||||||
import type { Account } from '../../types/resources';
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
|
||||||
import { autoPlayGif } from '../initial_state';
|
import { autoPlayGif } from '../initial_state';
|
||||||
|
|
||||||
import { Skeleton } from './skeleton';
|
import { Skeleton } from './skeleton';
|
||||||
|
|
|
@ -2,6 +2,8 @@ import classNames from 'classnames';
|
||||||
|
|
||||||
import { ReactComponent as CheckBoxOutlineBlankIcon } from '@material-symbols/svg-600/outlined/check_box_outline_blank.svg';
|
import { ReactComponent as CheckBoxOutlineBlankIcon } from '@material-symbols/svg-600/outlined/check_box_outline_blank.svg';
|
||||||
|
|
||||||
|
import { isProduction } from 'mastodon/utils/environment';
|
||||||
|
|
||||||
interface SVGPropsWithTitle extends React.SVGProps<SVGSVGElement> {
|
interface SVGPropsWithTitle extends React.SVGProps<SVGSVGElement> {
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
@ -24,7 +26,7 @@ export const Icon: React.FC<Props> = ({
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (!IconComponent) {
|
if (!IconComponent) {
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (!isProduction()) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`<Icon id="${id}" className="${className}"> is missing an "icon" prop.`,
|
`<Icon id="${id}" className="${className}"> is missing an "icon" prop.`,
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,7 +19,7 @@ const makeMapStateToProps = () => {
|
||||||
class InlineAccount extends PureComponent {
|
class InlineAccount extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
||||||
import { createBrowserHistory } from 'history';
|
import { createBrowserHistory } from 'history';
|
||||||
|
|
||||||
import { layoutFromWindow } from 'mastodon/is_mobile';
|
import { layoutFromWindow } from 'mastodon/is_mobile';
|
||||||
|
import { isDevelopment } from 'mastodon/utils/environment';
|
||||||
|
|
||||||
interface MastodonLocationState {
|
interface MastodonLocationState {
|
||||||
fromMastodon?: boolean;
|
fromMastodon?: boolean;
|
||||||
|
@ -40,7 +41,7 @@ function normalizePath(
|
||||||
} else if (
|
} else if (
|
||||||
location.state !== undefined &&
|
location.state !== undefined &&
|
||||||
state !== undefined &&
|
state !== undefined &&
|
||||||
process.env.NODE_ENV === 'development'
|
isDevelopment()
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(
|
console.log(
|
||||||
|
|
|
@ -80,7 +80,7 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
status: ImmutablePropTypes.map,
|
status: ImmutablePropTypes.map,
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.record,
|
||||||
previousId: PropTypes.string,
|
previousId: PropTypes.string,
|
||||||
nextInReplyToId: PropTypes.string,
|
nextInReplyToId: PropTypes.string,
|
||||||
rootId: PropTypes.string,
|
rootId: PropTypes.string,
|
||||||
|
|
|
@ -17,8 +17,9 @@ import UI from 'mastodon/features/ui';
|
||||||
import initialState, { title as siteTitle } from 'mastodon/initial_state';
|
import initialState, { title as siteTitle } from 'mastodon/initial_state';
|
||||||
import { IntlProvider } from 'mastodon/locales';
|
import { IntlProvider } from 'mastodon/locales';
|
||||||
import { store } from 'mastodon/store';
|
import { store } from 'mastodon/store';
|
||||||
|
import { isProduction } from 'mastodon/utils/environment';
|
||||||
|
|
||||||
const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
|
const title = isProduction() ? siteTitle : `${siteTitle} (Dev)`;
|
||||||
|
|
||||||
const hydrateAction = hydrateStore(initialState);
|
const hydrateAction = hydrateStore(initialState);
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ class InlineAlert extends PureComponent {
|
||||||
class AccountNote extends ImmutablePureComponent {
|
class AccountNote extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
onSave: PropTypes.func.isRequired,
|
onSave: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -15,7 +15,7 @@ const messages = defineMessages({
|
||||||
class FeaturedTags extends ImmutablePureComponent {
|
class FeaturedTags extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.record,
|
||||||
featuredTags: ImmutablePropTypes.list,
|
featuredTags: ImmutablePropTypes.list,
|
||||||
tagged: PropTypes.string,
|
tagged: PropTypes.string,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { Icon } from 'mastodon/components/icon';
|
||||||
export default class FollowRequestNote extends ImmutablePureComponent {
|
export default class FollowRequestNote extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|
|
@ -91,7 +91,7 @@ const dateFormatOptions = {
|
||||||
class Header extends ImmutablePureComponent {
|
class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.record,
|
||||||
identity_props: ImmutablePropTypes.list,
|
identity_props: ImmutablePropTypes.list,
|
||||||
onFollow: PropTypes.func.isRequired,
|
onFollow: PropTypes.func.isRequired,
|
||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -17,7 +17,7 @@ import MovedNote from './moved_note';
|
||||||
class Header extends ImmutablePureComponent {
|
class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.record,
|
||||||
onFollow: PropTypes.func.isRequired,
|
onFollow: PropTypes.func.isRequired,
|
||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { revealAccount } from 'mastodon/actions/accounts';
|
|
||||||
import { Button } from 'mastodon/components/button';
|
|
||||||
import { domain } from 'mastodon/initial_state';
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { accountId }) => ({
|
|
||||||
|
|
||||||
reveal () {
|
|
||||||
dispatch(revealAccount(accountId));
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
class LimitedAccountHint extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
accountId: PropTypes.string.isRequired,
|
|
||||||
reveal: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { reveal } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='limited-account-hint'>
|
|
||||||
<p><FormattedMessage id='limited_account_hint.title' defaultMessage='This profile has been hidden by the moderators of {domain}.' values={{ domain }} /></p>
|
|
||||||
<Button onClick={reveal}><FormattedMessage id='limited_account_hint.action' defaultMessage='Show profile anyway' /></Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(() => {}, mapDispatchToProps)(LimitedAccountHint);
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { revealAccount } from 'mastodon/actions/accounts_typed';
|
||||||
|
import { Button } from 'mastodon/components/button';
|
||||||
|
import { domain } from 'mastodon/initial_state';
|
||||||
|
import { useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
export const LimitedAccountHint: React.FC<{ accountId: string }> = ({
|
||||||
|
accountId,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const reveal = useCallback(() => {
|
||||||
|
dispatch(revealAccount({ id: accountId }));
|
||||||
|
}, [dispatch, accountId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='limited-account-hint'>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='limited_account_hint.title'
|
||||||
|
defaultMessage='This profile has been hidden by the moderators of {domain}.'
|
||||||
|
values={{ domain }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<Button onClick={reveal}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='limited_account_hint.action'
|
||||||
|
defaultMessage='Show profile anyway'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -21,7 +21,7 @@ import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import StatusList from '../../components/status_list';
|
import StatusList from '../../components/status_list';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
import LimitedAccountHint from './components/limited_account_hint';
|
import { LimitedAccountHint } from './components/limited_account_hint';
|
||||||
import HeaderContainer from './containers/header_container';
|
import HeaderContainer from './containers/header_container';
|
||||||
|
|
||||||
const emptyList = ImmutableList();
|
const emptyList = ImmutableList();
|
||||||
|
|
|
@ -28,7 +28,7 @@ const messages = defineMessages({
|
||||||
class ActionBar extends PureComponent {
|
class ActionBar extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
onLogout: PropTypes.func.isRequired,
|
onLogout: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { DisplayName } from '../../../components/display_name';
|
||||||
export default class AutosuggestAccount extends ImmutablePureComponent {
|
export default class AutosuggestAccount extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { createRef } from 'react';
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
|
@ -80,6 +81,11 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
highlighted: false,
|
highlighted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.textareaRef = createRef(null);
|
||||||
|
}
|
||||||
|
|
||||||
handleChange = (e) => {
|
handleChange = (e) => {
|
||||||
this.props.onChange(e.target.value);
|
this.props.onChange(e.target.value);
|
||||||
};
|
};
|
||||||
|
@ -103,10 +109,10 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSubmit = (e) => {
|
handleSubmit = (e) => {
|
||||||
if (this.props.text !== this.autosuggestTextarea.textarea.value) {
|
if (this.props.text !== this.textareaRef.current.value) {
|
||||||
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
||||||
// Update the state to match the current text
|
// Update the state to match the current text
|
||||||
this.props.onChange(this.autosuggestTextarea.textarea.value);
|
this.props.onChange(this.textareaRef.current.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.canSubmit()) {
|
if (!this.canSubmit()) {
|
||||||
|
@ -185,26 +191,22 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
// immediately selectable, we have to wait for observers to run, as
|
// immediately selectable, we have to wait for observers to run, as
|
||||||
// described in https://github.com/WICG/inert#performance-and-gotchas
|
// described in https://github.com/WICG/inert#performance-and-gotchas
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
|
this.textareaRef.current.setSelectionRange(selectionStart, selectionEnd);
|
||||||
this.autosuggestTextarea.textarea.focus();
|
this.textareaRef.current.focus();
|
||||||
this.setState({ highlighted: true });
|
this.setState({ highlighted: true });
|
||||||
this.timeout = setTimeout(() => this.setState({ highlighted: false }), 700);
|
this.timeout = setTimeout(() => this.setState({ highlighted: false }), 700);
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
} else if(prevProps.isSubmitting && !this.props.isSubmitting) {
|
} else if(prevProps.isSubmitting && !this.props.isSubmitting) {
|
||||||
this.autosuggestTextarea.textarea.focus();
|
this.textareaRef.current.focus();
|
||||||
} else if (this.props.spoiler !== prevProps.spoiler) {
|
} else if (this.props.spoiler !== prevProps.spoiler) {
|
||||||
if (this.props.spoiler) {
|
if (this.props.spoiler) {
|
||||||
this.spoilerText.input.focus();
|
this.spoilerText.input.focus();
|
||||||
} else if (prevProps.spoiler) {
|
} else if (prevProps.spoiler) {
|
||||||
this.autosuggestTextarea.textarea.focus();
|
this.textareaRef.current.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setAutosuggestTextarea = (c) => {
|
|
||||||
this.autosuggestTextarea = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
setSpoilerText = (c) => {
|
setSpoilerText = (c) => {
|
||||||
this.spoilerText = c;
|
this.spoilerText = c;
|
||||||
};
|
};
|
||||||
|
@ -215,7 +217,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleEmojiPick = (data) => {
|
handleEmojiPick = (data) => {
|
||||||
const { text } = this.props;
|
const { text } = this.props;
|
||||||
const position = this.autosuggestTextarea.textarea.selectionStart;
|
const position = this.textareaRef.current.selectionStart;
|
||||||
const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
|
const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
|
||||||
|
|
||||||
this.props.onPickEmoji(position, data, needsSpace);
|
this.props.onPickEmoji(position, data, needsSpace);
|
||||||
|
@ -264,7 +266,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className={classNames('compose-form__highlightable', { active: highlighted })}>
|
<div className={classNames('compose-form__highlightable', { active: highlighted })}>
|
||||||
<AutosuggestTextarea
|
<AutosuggestTextarea
|
||||||
ref={this.setAutosuggestTextarea}
|
ref={this.textareaRef}
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={this.props.text}
|
value={this.props.text}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import ActionBar from './action_bar';
|
||||||
export default class NavigationBar extends ImmutablePureComponent {
|
export default class NavigationBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
onLogout: PropTypes.func.isRequired,
|
onLogout: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func,
|
onClose: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
|
@ -102,7 +102,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
class AccountCard extends ImmutablePureComponent {
|
class AccountCard extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
onFollow: PropTypes.func.isRequired,
|
onFollow: PropTypes.func.isRequired,
|
||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -22,7 +22,7 @@ const messages = defineMessages({
|
||||||
class AccountAuthorize extends ImmutablePureComponent {
|
class AccountAuthorize extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
onAuthorize: PropTypes.func.isRequired,
|
onAuthorize: PropTypes.func.isRequired,
|
||||||
onReject: PropTypes.func.isRequired,
|
onReject: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { ColumnBackButton } from '../../components/column_back_button';
|
||||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import AccountContainer from '../../containers/account_container';
|
import AccountContainer from '../../containers/account_container';
|
||||||
import LimitedAccountHint from '../account_timeline/components/limited_account_hint';
|
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
||||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { ColumnBackButton } from '../../components/column_back_button';
|
||||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import AccountContainer from '../../containers/account_container';
|
import AccountContainer from '../../containers/account_container';
|
||||||
import LimitedAccountHint from '../account_timeline/components/limited_account_hint';
|
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
||||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ const makeMapStateToProps = () => {
|
||||||
class Account extends ImmutablePureComponent {
|
class Account extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|
|
@ -39,7 +39,7 @@ const mapDispatchToProps = (dispatch, { accountId }) => ({
|
||||||
class Account extends ImmutablePureComponent {
|
class Account extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
onRemove: PropTypes.func.isRequired,
|
onRemove: PropTypes.func.isRequired,
|
||||||
onAdd: PropTypes.func.isRequired,
|
onAdd: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -22,7 +22,7 @@ const messages = defineMessages({
|
||||||
class FollowRequest extends ImmutablePureComponent {
|
class FollowRequest extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
onAuthorize: PropTypes.func.isRequired,
|
onAuthorize: PropTypes.func.isRequired,
|
||||||
onReject: PropTypes.func.isRequired,
|
onReject: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -20,7 +20,7 @@ const messages = defineMessages({
|
||||||
class Report extends ImmutablePureComponent {
|
class Report extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
report: ImmutablePropTypes.map.isRequired,
|
report: ImmutablePropTypes.map.isRequired,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -46,7 +46,7 @@ const mapStateToProps = () => {
|
||||||
class Onboarding extends ImmutablePureComponent {
|
class Onboarding extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.record,
|
||||||
...WithRouterPropTypes,
|
...WithRouterPropTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -145,7 +145,7 @@ class Share extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onBack: PropTypes.func,
|
onBack: PropTypes.func,
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.record,
|
||||||
intl: PropTypes.object,
|
intl: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Header extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accountId: PropTypes.string.isRequired,
|
accountId: PropTypes.string.isRequired,
|
||||||
statusId: PropTypes.string.isRequired,
|
statusId: PropTypes.string.isRequired,
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Thanks extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
submitted: PropTypes.bool,
|
submitted: PropTypes.bool,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -110,7 +110,7 @@ class FocalPointModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
media: ImmutablePropTypes.map.isRequired,
|
media: ImmutablePropTypes.map.isRequired,
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
isUploadingThumbnail: PropTypes.bool,
|
isUploadingThumbnail: PropTypes.bool,
|
||||||
onSave: PropTypes.func.isRequired,
|
onSave: PropTypes.func.isRequired,
|
||||||
onChangeDescription: PropTypes.func.isRequired,
|
onChangeDescription: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -41,7 +41,7 @@ class ReportModal extends ImmutablePureComponent {
|
||||||
statusId: PropTypes.string,
|
statusId: PropTypes.string,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.record.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
|
|
@ -1,43 +1,5 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef Emoji
|
|
||||||
* @property {string} shortcode
|
|
||||||
* @property {string} static_url
|
|
||||||
* @property {string} url
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef AccountField
|
|
||||||
* @property {string} name
|
|
||||||
* @property {string} value
|
|
||||||
* @property {string} verified_at
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef Account
|
|
||||||
* @property {string} acct
|
|
||||||
* @property {string} avatar
|
|
||||||
* @property {string} avatar_static
|
|
||||||
* @property {boolean} bot
|
|
||||||
* @property {string} created_at
|
|
||||||
* @property {boolean=} discoverable
|
|
||||||
* @property {string} display_name
|
|
||||||
* @property {Emoji[]} emojis
|
|
||||||
* @property {AccountField[]} fields
|
|
||||||
* @property {number} followers_count
|
|
||||||
* @property {number} following_count
|
|
||||||
* @property {boolean} group
|
|
||||||
* @property {string} header
|
|
||||||
* @property {string} header_static
|
|
||||||
* @property {string} id
|
|
||||||
* @property {string=} last_status_at
|
|
||||||
* @property {boolean} locked
|
|
||||||
* @property {string} note
|
|
||||||
* @property {number} statuses_count
|
|
||||||
* @property {string} url
|
|
||||||
* @property {string} username
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {[code: string, name: string, localName: string]} InitialStateLanguage
|
* @typedef {[code: string, name: string, localName: string]} InitialStateLanguage
|
||||||
|
@ -85,7 +47,7 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef InitialState
|
* @typedef InitialState
|
||||||
* @property {Record<string, Account>} accounts
|
* @property {Record<string, import("./api_types/accounts").ApiAccountJSON>} accounts
|
||||||
* @property {InitialStateLanguage[]} languages
|
* @property {InitialStateLanguage[]} languages
|
||||||
* @property {boolean=} critical_updates_pending
|
* @property {boolean=} critical_updates_pending
|
||||||
* @property {InitialStateMeta} meta
|
* @property {InitialStateMeta} meta
|
||||||
|
|
|
@ -425,7 +425,7 @@
|
||||||
"notification.admin.report": "{name} meldete {target}",
|
"notification.admin.report": "{name} meldete {target}",
|
||||||
"notification.admin.sign_up": "{name} registrierte sich",
|
"notification.admin.sign_up": "{name} registrierte sich",
|
||||||
"notification.favourite": "{name} favorisierte deinen Beitrag",
|
"notification.favourite": "{name} favorisierte deinen Beitrag",
|
||||||
"notification.follow": "{name} folgt dir jetzt",
|
"notification.follow": "{name} folgt dir",
|
||||||
"notification.follow_request": "{name} möchte dir folgen",
|
"notification.follow_request": "{name} möchte dir folgen",
|
||||||
"notification.mention": "{name} erwähnte dich",
|
"notification.mention": "{name} erwähnte dich",
|
||||||
"notification.own_poll": "Deine Umfrage ist beendet",
|
"notification.own_poll": "Deine Umfrage ist beendet",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { isDevelopment } from 'mastodon/utils/environment';
|
||||||
|
|
||||||
export interface LocaleData {
|
export interface LocaleData {
|
||||||
locale: string;
|
locale: string;
|
||||||
messages: Record<string, string>;
|
messages: Record<string, string>;
|
||||||
|
@ -11,7 +13,7 @@ export function setLocale(locale: LocaleData) {
|
||||||
|
|
||||||
export function getLocale(): LocaleData {
|
export function getLocale(): LocaleData {
|
||||||
if (!loadedLocale) {
|
if (!loadedLocale) {
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (isDevelopment()) {
|
||||||
throw new Error('getLocale() called before any locale has been set');
|
throw new Error('getLocale() called before any locale has been set');
|
||||||
} else {
|
} else {
|
||||||
return { locale: 'unknown', messages: {} };
|
return { locale: 'unknown', messages: {} };
|
||||||
|
|
|
@ -202,7 +202,7 @@
|
||||||
"dismissable_banner.community_timeline": "אלו הם החצרוצים הציבוריים האחרונים מהמשתמשים על שרת {domain}.",
|
"dismissable_banner.community_timeline": "אלו הם החצרוצים הציבוריים האחרונים מהמשתמשים על שרת {domain}.",
|
||||||
"dismissable_banner.dismiss": "בטל",
|
"dismissable_banner.dismiss": "בטל",
|
||||||
"dismissable_banner.explore_links": "אלו הקישורים האחרונים ששותפו על ידי משתמשים ששרת זה רואה ברשת המבוזרת כרגע.",
|
"dismissable_banner.explore_links": "אלו הקישורים האחרונים ששותפו על ידי משתמשים ששרת זה רואה ברשת המבוזרת כרגע.",
|
||||||
"dismissable_banner.explore_statuses": "ההודעות האלו, משרת זה ואחרים ברשת המבוזרת, צוברים חשיפה היום. הודעות חדשות יותר עם יותר הדהודים וחיבובים מדורגות גבוה יותר.",
|
"dismissable_banner.explore_statuses": "אלו הודעות משרת זה ואחרים ברשת המבוזרת שצוברות חשיפה היום. הודעות חדשות יותר עם יותר הדהודים וחיבובים מדורגות גבוה יותר.",
|
||||||
"dismissable_banner.explore_tags": "התגיות האלו, משרת זה ואחרים ברשת המבוזרת, צוברות חשיפה כעת.",
|
"dismissable_banner.explore_tags": "התגיות האלו, משרת זה ואחרים ברשת המבוזרת, צוברות חשיפה כעת.",
|
||||||
"dismissable_banner.public_timeline": "אלו ההודעות האחרונות שהתקבלו מהמשתמשים שנעקבים על ידי משתמשים מ־{domain}.",
|
"dismissable_banner.public_timeline": "אלו ההודעות האחרונות שהתקבלו מהמשתמשים שנעקבים על ידי משתמשים מ־{domain}.",
|
||||||
"embed.instructions": "ניתן להטמיע את ההודעה הזו באתרך ע\"י העתקת הקוד שלהלן.",
|
"embed.instructions": "ניתן להטמיע את ההודעה הזו באתרך ע\"י העתקת הקוד שלהלן.",
|
||||||
|
@ -630,7 +630,7 @@
|
||||||
"status.edited": "נערך ב{date}",
|
"status.edited": "נערך ב{date}",
|
||||||
"status.edited_x_times": "נערך {count, plural, one {פעם {count}} other {{count} פעמים}}",
|
"status.edited_x_times": "נערך {count, plural, one {פעם {count}} other {{count} פעמים}}",
|
||||||
"status.embed": "הטמעה",
|
"status.embed": "הטמעה",
|
||||||
"status.favourite": "מחובבת",
|
"status.favourite": "חיבוב",
|
||||||
"status.filter": "סנן הודעה זו",
|
"status.filter": "סנן הודעה זו",
|
||||||
"status.filtered": "סונן",
|
"status.filtered": "סונן",
|
||||||
"status.hide": "הסתרת חיצרוץ",
|
"status.hide": "הסתרת חיצרוץ",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"about.contact": "Kontakt:",
|
"about.contact": "Kontakt:",
|
||||||
|
"about.domain_blocks.no_reason_available": "Razlog nije dostupan",
|
||||||
"account.account_note_header": "Bilješka",
|
"account.account_note_header": "Bilješka",
|
||||||
"account.add_or_remove_from_list": "Dodaj ili ukloni s liste",
|
"account.add_or_remove_from_list": "Dodaj ili ukloni s liste",
|
||||||
"account.badges.bot": "Bot",
|
"account.badges.bot": "Bot",
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
"account.edit_profile": "Uredi profil",
|
"account.edit_profile": "Uredi profil",
|
||||||
"account.enable_notifications": "Obavjesti me kada @{name} napravi objavu",
|
"account.enable_notifications": "Obavjesti me kada @{name} napravi objavu",
|
||||||
"account.endorse": "Istakni na profilu",
|
"account.endorse": "Istakni na profilu",
|
||||||
|
"account.featured_tags.last_status_never": "Nema postova",
|
||||||
"account.follow": "Prati",
|
"account.follow": "Prati",
|
||||||
"account.followers": "Pratitelji",
|
"account.followers": "Pratitelji",
|
||||||
"account.followers.empty": "Nitko još ne prati korisnika/cu.",
|
"account.followers.empty": "Nitko još ne prati korisnika/cu.",
|
||||||
|
@ -21,13 +23,18 @@
|
||||||
"account.following_counter": "{count, plural, one {{counter} praćeni} few{{counter} praćena} other {{counter} praćenih}}",
|
"account.following_counter": "{count, plural, one {{counter} praćeni} few{{counter} praćena} other {{counter} praćenih}}",
|
||||||
"account.follows.empty": "Korisnik/ca još ne prati nikoga.",
|
"account.follows.empty": "Korisnik/ca još ne prati nikoga.",
|
||||||
"account.follows_you": "Prati te",
|
"account.follows_you": "Prati te",
|
||||||
|
"account.go_to_profile": "Idi na profil",
|
||||||
"account.hide_reblogs": "Sakrij boostove od @{name}",
|
"account.hide_reblogs": "Sakrij boostove od @{name}",
|
||||||
|
"account.in_memoriam": "U sjećanje.",
|
||||||
"account.link_verified_on": "Vlasništvo ove poveznice provjereno je {date}",
|
"account.link_verified_on": "Vlasništvo ove poveznice provjereno je {date}",
|
||||||
"account.locked_info": "Status privatnosti ovog računa postavljen je na zaključano. Vlasnik ručno pregledava tko ih može pratiti.",
|
"account.locked_info": "Status privatnosti ovog računa postavljen je na zaključano. Vlasnik ručno pregledava tko ih može pratiti.",
|
||||||
"account.media": "Medijski sadržaj",
|
"account.media": "Medijski sadržaj",
|
||||||
"account.mention": "Spomeni @{name}",
|
"account.mention": "Spomeni @{name}",
|
||||||
"account.mute": "Utišaj @{name}",
|
"account.mute": "Utišaj @{name}",
|
||||||
|
"account.mute_notifications_short": "Utišaj obavijesti",
|
||||||
|
"account.mute_short": "Utišaj",
|
||||||
"account.muted": "Utišano",
|
"account.muted": "Utišano",
|
||||||
|
"account.open_original_page": "Otvori originalnu stranicu",
|
||||||
"account.posts": "Objave",
|
"account.posts": "Objave",
|
||||||
"account.posts_with_replies": "Objave i odgovori",
|
"account.posts_with_replies": "Objave i odgovori",
|
||||||
"account.report": "Prijavi @{name}",
|
"account.report": "Prijavi @{name}",
|
||||||
|
@ -52,6 +59,7 @@
|
||||||
"alert.unexpected.title": "Ups!",
|
"alert.unexpected.title": "Ups!",
|
||||||
"announcement.announcement": "Najava",
|
"announcement.announcement": "Najava",
|
||||||
"attachments_list.unprocessed": "(neobrađeno)",
|
"attachments_list.unprocessed": "(neobrađeno)",
|
||||||
|
"audio.hide": "Sakrij audio",
|
||||||
"autosuggest_hashtag.per_week": "{count} tjedno",
|
"autosuggest_hashtag.per_week": "{count} tjedno",
|
||||||
"boost_modal.combo": "Možete pritisnuti {combo} kako biste preskočili ovo sljedeći put",
|
"boost_modal.combo": "Možete pritisnuti {combo} kako biste preskočili ovo sljedeći put",
|
||||||
"bundle_column_error.error.title": "Oh, ne!",
|
"bundle_column_error.error.title": "Oh, ne!",
|
||||||
|
@ -66,6 +74,7 @@
|
||||||
"column.community": "Lokalna vremenska crta",
|
"column.community": "Lokalna vremenska crta",
|
||||||
"column.directory": "Pregledavanje profila",
|
"column.directory": "Pregledavanje profila",
|
||||||
"column.domain_blocks": "Blokirane domene",
|
"column.domain_blocks": "Blokirane domene",
|
||||||
|
"column.favourites": "Favoriti",
|
||||||
"column.follow_requests": "Zahtjevi za praćenje",
|
"column.follow_requests": "Zahtjevi za praćenje",
|
||||||
"column.home": "Početna",
|
"column.home": "Početna",
|
||||||
"column.lists": "Liste",
|
"column.lists": "Liste",
|
||||||
|
@ -86,6 +95,8 @@
|
||||||
"community.column_settings.remote_only": "Samo udaljeno",
|
"community.column_settings.remote_only": "Samo udaljeno",
|
||||||
"compose.language.change": "Promijeni jezik",
|
"compose.language.change": "Promijeni jezik",
|
||||||
"compose.language.search": "Pretraži jezike...",
|
"compose.language.search": "Pretraži jezike...",
|
||||||
|
"compose.published.open": "Otvori",
|
||||||
|
"compose.saved.body": "Post spremljen.",
|
||||||
"compose_form.direct_message_warning_learn_more": "Saznajte više",
|
"compose_form.direct_message_warning_learn_more": "Saznajte više",
|
||||||
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
|
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
|
||||||
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
|
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
|
||||||
|
@ -179,6 +190,8 @@
|
||||||
"errors.unexpected_crash.copy_stacktrace": "Kopiraj stacktrace u međuspremnik",
|
"errors.unexpected_crash.copy_stacktrace": "Kopiraj stacktrace u međuspremnik",
|
||||||
"errors.unexpected_crash.report_issue": "Prijavi problem",
|
"errors.unexpected_crash.report_issue": "Prijavi problem",
|
||||||
"explore.search_results": "Rezultati pretrage",
|
"explore.search_results": "Rezultati pretrage",
|
||||||
|
"explore.suggested_follows": "Ljudi",
|
||||||
|
"explore.title": "Pretraži",
|
||||||
"explore.trending_links": "Novosti",
|
"explore.trending_links": "Novosti",
|
||||||
"explore.trending_statuses": "Objave",
|
"explore.trending_statuses": "Objave",
|
||||||
"explore.trending_tags": "Hashtagovi",
|
"explore.trending_tags": "Hashtagovi",
|
||||||
|
@ -189,12 +202,17 @@
|
||||||
"filter_modal.select_filter.subtitle": "Odaberite postojeću kategoriju ili stvorite novu",
|
"filter_modal.select_filter.subtitle": "Odaberite postojeću kategoriju ili stvorite novu",
|
||||||
"filter_modal.select_filter.title": "Filtriraj ovu objavu",
|
"filter_modal.select_filter.title": "Filtriraj ovu objavu",
|
||||||
"filter_modal.title.status": "Filtriraj objavu",
|
"filter_modal.title.status": "Filtriraj objavu",
|
||||||
|
"firehose.all": "Sve",
|
||||||
|
"firehose.local": "Ovaj server",
|
||||||
"follow_request.authorize": "Autoriziraj",
|
"follow_request.authorize": "Autoriziraj",
|
||||||
"follow_request.reject": "Odbij",
|
"follow_request.reject": "Odbij",
|
||||||
|
"footer.about": "O aplikaciji",
|
||||||
"footer.get_app": "Preuzmi aplikaciju",
|
"footer.get_app": "Preuzmi aplikaciju",
|
||||||
|
"footer.invite": "Pozovi ljude",
|
||||||
"footer.keyboard_shortcuts": "Tipkovni prečaci",
|
"footer.keyboard_shortcuts": "Tipkovni prečaci",
|
||||||
"footer.privacy_policy": "Pravila o zaštiti privatnosti",
|
"footer.privacy_policy": "Pravila o zaštiti privatnosti",
|
||||||
"footer.source_code": "Prikaz izvornog koda",
|
"footer.source_code": "Prikaz izvornog koda",
|
||||||
|
"footer.status": "Stanje",
|
||||||
"generic.saved": "Spremljeno",
|
"generic.saved": "Spremljeno",
|
||||||
"getting_started.heading": "Počnimo",
|
"getting_started.heading": "Počnimo",
|
||||||
"hashtag.column_header.tag_mode.all": "i {additional}",
|
"hashtag.column_header.tag_mode.all": "i {additional}",
|
||||||
|
@ -212,7 +230,11 @@
|
||||||
"home.column_settings.show_reblogs": "Pokaži boostove",
|
"home.column_settings.show_reblogs": "Pokaži boostove",
|
||||||
"home.column_settings.show_replies": "Pokaži odgovore",
|
"home.column_settings.show_replies": "Pokaži odgovore",
|
||||||
"home.hide_announcements": "Sakrij najave",
|
"home.hide_announcements": "Sakrij najave",
|
||||||
|
"home.pending_critical_update.title": "Dostupno je kritično sigurnosno ažuriranje!",
|
||||||
"home.show_announcements": "Prikaži najave",
|
"home.show_announcements": "Prikaži najave",
|
||||||
|
"interaction_modal.login.action": "Odvedi me kući",
|
||||||
|
"interaction_modal.no_account_yet": "Nisi na Mastodonu?",
|
||||||
|
"interaction_modal.on_this_server": "Na ovom serveru",
|
||||||
"intervals.full.days": "{number, plural, one {# dan} other {# dana}}",
|
"intervals.full.days": "{number, plural, one {# dan} other {# dana}}",
|
||||||
"intervals.full.hours": "{number, plural, one {# sat} few {# sata} other {# sati}}",
|
"intervals.full.hours": "{number, plural, one {# sat} few {# sata} other {# sati}}",
|
||||||
"intervals.full.minutes": "{number, plural, one {# minuta} few {# minute} other {# minuta}}",
|
"intervals.full.minutes": "{number, plural, one {# minuta} few {# minute} other {# minuta}}",
|
||||||
|
|
|
@ -2,12 +2,14 @@ import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { IntlProvider as BaseIntlProvider } from 'react-intl';
|
import { IntlProvider as BaseIntlProvider } from 'react-intl';
|
||||||
|
|
||||||
|
import { isProduction } from 'mastodon/utils/environment';
|
||||||
|
|
||||||
import { getLocale, isLocaleLoaded } from './global_locale';
|
import { getLocale, isLocaleLoaded } from './global_locale';
|
||||||
import { loadLocale } from './load_locale';
|
import { loadLocale } from './load_locale';
|
||||||
|
|
||||||
function onProviderError(error: unknown) {
|
function onProviderError(error: unknown) {
|
||||||
// Silent the error, like upstream does
|
// Silent the error, like upstream does
|
||||||
if (process.env.NODE_ENV === 'production') return;
|
if (isProduction()) return;
|
||||||
|
|
||||||
// This browser does not advertise Intl support for this locale, we only print a warning
|
// This browser does not advertise Intl support for this locale, we only print a warning
|
||||||
// As-per the spec, the browser should select the best matching locale
|
// As-per the spec, the browser should select the best matching locale
|
||||||
|
|
|
@ -61,12 +61,12 @@
|
||||||
"account.requested_follow": "{name} vam želi slediti",
|
"account.requested_follow": "{name} vam želi slediti",
|
||||||
"account.share": "Deli profil osebe @{name}",
|
"account.share": "Deli profil osebe @{name}",
|
||||||
"account.show_reblogs": "Pokaži izpostavitve osebe @{name}",
|
"account.show_reblogs": "Pokaži izpostavitve osebe @{name}",
|
||||||
"account.statuses_counter": "{count, plural, one {{count} tut} two {{count} tuta} few {{count} tuti} other {{count} tutov}}",
|
"account.statuses_counter": "{count, plural, one {{count} objava} two {{count} objavi} few {{count} objave} other {{count} objav}}",
|
||||||
"account.unblock": "Odblokiraj @{name}",
|
"account.unblock": "Odblokiraj @{name}",
|
||||||
"account.unblock_domain": "Odblokiraj domeno {domain}",
|
"account.unblock_domain": "Odblokiraj domeno {domain}",
|
||||||
"account.unblock_short": "Odblokiraj",
|
"account.unblock_short": "Odblokiraj",
|
||||||
"account.unendorse": "Ne vključi v profil",
|
"account.unendorse": "Ne vključi v profil",
|
||||||
"account.unfollow": "Prenehaj slediti",
|
"account.unfollow": "Ne sledi več",
|
||||||
"account.unmute": "Odtišaj @{name}",
|
"account.unmute": "Odtišaj @{name}",
|
||||||
"account.unmute_notifications_short": "Izklopi utišanje obvestil",
|
"account.unmute_notifications_short": "Izklopi utišanje obvestil",
|
||||||
"account.unmute_short": "Odtišaj",
|
"account.unmute_short": "Odtišaj",
|
||||||
|
@ -185,7 +185,7 @@
|
||||||
"confirmations.redraft.message": "Ali ste prepričani, da želite izbrisati ta status in ga preoblikovati? Vzljubi in izpostavitve bodo izgubljeni, odgovori na izvirno objavo pa bodo osiroteli.",
|
"confirmations.redraft.message": "Ali ste prepričani, da želite izbrisati ta status in ga preoblikovati? Vzljubi in izpostavitve bodo izgubljeni, odgovori na izvirno objavo pa bodo osiroteli.",
|
||||||
"confirmations.reply.confirm": "Odgovori",
|
"confirmations.reply.confirm": "Odgovori",
|
||||||
"confirmations.reply.message": "Odgovarjanje bo prepisalo sporočilo, ki ga trenutno sestavljate. Ali ste prepričani, da želite nadaljevati?",
|
"confirmations.reply.message": "Odgovarjanje bo prepisalo sporočilo, ki ga trenutno sestavljate. Ali ste prepričani, da želite nadaljevati?",
|
||||||
"confirmations.unfollow.confirm": "Prenehaj slediti",
|
"confirmations.unfollow.confirm": "Ne sledi več",
|
||||||
"confirmations.unfollow.message": "Ali ste prepričani, da ne želite več slediti {name}?",
|
"confirmations.unfollow.message": "Ali ste prepričani, da ne želite več slediti {name}?",
|
||||||
"conversation.delete": "Izbriši pogovor",
|
"conversation.delete": "Izbriši pogovor",
|
||||||
"conversation.mark_as_read": "Označi kot prebrano",
|
"conversation.mark_as_read": "Označi kot prebrano",
|
||||||
|
@ -301,7 +301,7 @@
|
||||||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} objava} two {{counter} objavi} few {{counter} objav} other {{counter} objav}}",
|
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} objava} two {{counter} objavi} few {{counter} objav} other {{counter} objav}}",
|
||||||
"hashtag.follow": "Sledi ključniku",
|
"hashtag.follow": "Sledi ključniku",
|
||||||
"hashtag.unfollow": "Nehaj slediti ključniku",
|
"hashtag.unfollow": "Nehaj slediti ključniku",
|
||||||
"hashtags.and_other": "…and {count, plural, one {} two {# več} few {# več}other {# več}}",
|
"hashtags.and_other": "…in še {count, plural, other {#}}",
|
||||||
"home.actions.go_to_explore": "Poglejte, kaj je v trendu",
|
"home.actions.go_to_explore": "Poglejte, kaj je v trendu",
|
||||||
"home.actions.go_to_suggestions": "Poiščite osebe, ki jim želite slediti",
|
"home.actions.go_to_suggestions": "Poiščite osebe, ki jim želite slediti",
|
||||||
"home.column_settings.basic": "Osnovno",
|
"home.column_settings.basic": "Osnovno",
|
||||||
|
|
|
@ -7,6 +7,8 @@ import * as perf from 'mastodon/performance';
|
||||||
import ready from 'mastodon/ready';
|
import ready from 'mastodon/ready';
|
||||||
import { store } from 'mastodon/store';
|
import { store } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { isProduction } from './utils/environment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
|
@ -21,7 +23,7 @@ function main() {
|
||||||
root.render(<Mastodon {...props} />);
|
root.render(<Mastodon {...props} />);
|
||||||
store.dispatch(setupBrowserNotifications());
|
store.dispatch(setupBrowserNotifications());
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production' && me && 'serviceWorker' in navigator) {
|
if (isProduction() && me && 'serviceWorker' in navigator) {
|
||||||
const { Workbox } = await import('workbox-window');
|
const { Workbox } = await import('workbox-window');
|
||||||
const wb = new Workbox('/sw.js');
|
const wb = new Workbox('/sw.js');
|
||||||
/** @type {ServiceWorkerRegistration} */
|
/** @type {ServiceWorkerRegistration} */
|
||||||
|
|
149
app/javascript/mastodon/models/account.ts
Normal file
149
app/javascript/mastodon/models/account.ts
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
import type { RecordOf } from 'immutable';
|
||||||
|
import { List, Record as ImmutableRecord } from 'immutable';
|
||||||
|
|
||||||
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiAccountFieldJSON,
|
||||||
|
ApiAccountRoleJSON,
|
||||||
|
ApiAccountJSON,
|
||||||
|
} from 'mastodon/api_types/accounts';
|
||||||
|
import type { ApiCustomEmojiJSON } from 'mastodon/api_types/custom_emoji';
|
||||||
|
import emojify from 'mastodon/features/emoji/emoji';
|
||||||
|
import { unescapeHTML } from 'mastodon/utils/html';
|
||||||
|
|
||||||
|
import { CustomEmojiFactory } from './custom_emoji';
|
||||||
|
import type { CustomEmoji } from './custom_emoji';
|
||||||
|
|
||||||
|
// AccountField
|
||||||
|
interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
|
||||||
|
name_emojified: string;
|
||||||
|
value_emojified: string;
|
||||||
|
value_plain: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountField = RecordOf<AccountFieldShape>;
|
||||||
|
|
||||||
|
const AccountFieldFactory = ImmutableRecord<AccountFieldShape>({
|
||||||
|
name: '',
|
||||||
|
value: '',
|
||||||
|
verified_at: null,
|
||||||
|
name_emojified: '',
|
||||||
|
value_emojified: '',
|
||||||
|
value_plain: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// AccountRole
|
||||||
|
export type AccountRoleShape = ApiAccountRoleJSON;
|
||||||
|
export type AccountRole = RecordOf<AccountRoleShape>;
|
||||||
|
|
||||||
|
const AccountRoleFactory = ImmutableRecord<AccountRoleShape>({
|
||||||
|
color: '',
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Account
|
||||||
|
export interface AccountShape
|
||||||
|
extends Required<
|
||||||
|
Omit<ApiAccountJSON, 'emojis' | 'fields' | 'roles' | 'moved'>
|
||||||
|
> {
|
||||||
|
emojis: List<CustomEmoji>;
|
||||||
|
fields: List<AccountField>;
|
||||||
|
roles: List<AccountRole>;
|
||||||
|
display_name_html: string;
|
||||||
|
note_emojified: string;
|
||||||
|
note_plain: string | null;
|
||||||
|
hidden: boolean;
|
||||||
|
moved: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Account = RecordOf<AccountShape>;
|
||||||
|
|
||||||
|
export const accountDefaultValues: AccountShape = {
|
||||||
|
acct: '',
|
||||||
|
avatar: '',
|
||||||
|
avatar_static: '',
|
||||||
|
bot: false,
|
||||||
|
created_at: '',
|
||||||
|
discoverable: false,
|
||||||
|
display_name: '',
|
||||||
|
display_name_html: '',
|
||||||
|
emojis: List<CustomEmoji>(),
|
||||||
|
fields: List<AccountField>(),
|
||||||
|
group: false,
|
||||||
|
header: '',
|
||||||
|
header_static: '',
|
||||||
|
id: '',
|
||||||
|
last_status_at: '',
|
||||||
|
locked: false,
|
||||||
|
noindex: false,
|
||||||
|
note: '',
|
||||||
|
note_emojified: '',
|
||||||
|
note_plain: 'string',
|
||||||
|
roles: List<AccountRole>(),
|
||||||
|
uri: '',
|
||||||
|
url: '',
|
||||||
|
username: '',
|
||||||
|
followers_count: 0,
|
||||||
|
following_count: 0,
|
||||||
|
statuses_count: 0,
|
||||||
|
hidden: false,
|
||||||
|
suspended: false,
|
||||||
|
memorial: false,
|
||||||
|
limited: false,
|
||||||
|
moved: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccountFactory = ImmutableRecord<AccountShape>(accountDefaultValues);
|
||||||
|
|
||||||
|
type EmojiMap = Record<string, ApiCustomEmojiJSON>;
|
||||||
|
|
||||||
|
function makeEmojiMap(emojis: ApiCustomEmojiJSON[]) {
|
||||||
|
return emojis.reduce<EmojiMap>((obj, emoji) => {
|
||||||
|
obj[`:${emoji.shortcode}:`] = emoji;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAccountField(
|
||||||
|
jsonField: ApiAccountFieldJSON,
|
||||||
|
emojiMap: EmojiMap,
|
||||||
|
) {
|
||||||
|
return AccountFieldFactory({
|
||||||
|
...jsonField,
|
||||||
|
name_emojified: emojify(
|
||||||
|
escapeTextContentForBrowser(jsonField.name),
|
||||||
|
emojiMap,
|
||||||
|
),
|
||||||
|
value_emojified: emojify(jsonField.value, emojiMap),
|
||||||
|
value_plain: unescapeHTML(jsonField.value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
||||||
|
const { moved, ...accountJSON } = serverJSON;
|
||||||
|
|
||||||
|
const emojiMap = makeEmojiMap(accountJSON.emojis);
|
||||||
|
|
||||||
|
const displayName =
|
||||||
|
accountJSON.display_name.trim().length === 0
|
||||||
|
? accountJSON.username
|
||||||
|
: accountJSON.display_name;
|
||||||
|
|
||||||
|
return AccountFactory({
|
||||||
|
...accountJSON,
|
||||||
|
moved: moved?.id,
|
||||||
|
fields: List(
|
||||||
|
serverJSON.fields.map((field) => createAccountField(field, emojiMap)),
|
||||||
|
),
|
||||||
|
emojis: List(serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji))),
|
||||||
|
roles: List(serverJSON.roles?.map((role) => AccountRoleFactory(role))),
|
||||||
|
display_name_html: emojify(
|
||||||
|
escapeTextContentForBrowser(displayName),
|
||||||
|
emojiMap,
|
||||||
|
),
|
||||||
|
note_emojified: emojify(accountJSON.note, emojiMap),
|
||||||
|
note_plain: unescapeHTML(accountJSON.note),
|
||||||
|
});
|
||||||
|
}
|
15
app/javascript/mastodon/models/custom_emoji.ts
Normal file
15
app/javascript/mastodon/models/custom_emoji.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import type { RecordOf } from 'immutable';
|
||||||
|
import { Record } from 'immutable';
|
||||||
|
|
||||||
|
import type { ApiCustomEmojiJSON } from 'mastodon/api_types/custom_emoji';
|
||||||
|
|
||||||
|
type CustomEmojiShape = Required<ApiCustomEmojiJSON>; // no changes from server shape
|
||||||
|
export type CustomEmoji = RecordOf<CustomEmojiShape>;
|
||||||
|
|
||||||
|
export const CustomEmojiFactory = Record<CustomEmojiShape>({
|
||||||
|
shortcode: '',
|
||||||
|
static_url: '',
|
||||||
|
url: '',
|
||||||
|
category: '',
|
||||||
|
visible_in_picker: false,
|
||||||
|
});
|
29
app/javascript/mastodon/models/relationship.ts
Normal file
29
app/javascript/mastodon/models/relationship.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import type { RecordOf } from 'immutable';
|
||||||
|
import { Record } from 'immutable';
|
||||||
|
|
||||||
|
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
|
||||||
|
|
||||||
|
type RelationshipShape = Required<ApiRelationshipJSON>; // no changes from server shape
|
||||||
|
export type Relationship = RecordOf<RelationshipShape>;
|
||||||
|
|
||||||
|
const RelationshipFactory = Record<RelationshipShape>({
|
||||||
|
blocked_by: false,
|
||||||
|
blocking: false,
|
||||||
|
domain_blocking: false,
|
||||||
|
endorsed: false,
|
||||||
|
followed_by: false,
|
||||||
|
following: false,
|
||||||
|
id: '',
|
||||||
|
languages: null,
|
||||||
|
muting_notifications: false,
|
||||||
|
muting: false,
|
||||||
|
note: '',
|
||||||
|
notifying: false,
|
||||||
|
requested_by: false,
|
||||||
|
requested: false,
|
||||||
|
showing_reblogs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createRelationship(attributes: Partial<RelationshipShape>) {
|
||||||
|
return RelationshipFactory(attributes);
|
||||||
|
}
|
|
@ -5,7 +5,9 @@
|
||||||
|
|
||||||
import * as marky from 'marky';
|
import * as marky from 'marky';
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
import { isDevelopment } from './utils/environment';
|
||||||
|
|
||||||
|
if (isDevelopment()) {
|
||||||
if (typeof performance !== 'undefined' && performance.setResourceTimingBufferSize) {
|
if (typeof performance !== 'undefined' && performance.setResourceTimingBufferSize) {
|
||||||
// Increase Firefox's performance entry limit; otherwise it's capped to 150.
|
// Increase Firefox's performance entry limit; otherwise it's capped to 150.
|
||||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1331135
|
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1331135
|
||||||
|
@ -18,13 +20,13 @@ if (process.env.NODE_ENV === 'development') {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function start(name) {
|
export function start(name) {
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (isDevelopment()) {
|
||||||
marky.mark(name);
|
marky.mark(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stop(name) {
|
export function stop(name) {
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (isDevelopment()) {
|
||||||
marky.stop(name);
|
marky.stop(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import 'core-js/features/object/assign';
|
|
||||||
import 'core-js/features/object/values';
|
|
||||||
import 'core-js/features/symbol';
|
|
||||||
import 'core-js/features/promise/finally';
|
|
||||||
import { decode as decodeBase64 } from '../utils/base64';
|
|
||||||
|
|
||||||
if (!Object.hasOwn(HTMLCanvasElement.prototype, 'toBlob')) {
|
|
||||||
const BASE64_MARKER = ';base64,';
|
|
||||||
|
|
||||||
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
|
|
||||||
value: function (
|
|
||||||
this: HTMLCanvasElement,
|
|
||||||
callback: BlobCallback,
|
|
||||||
type = 'image/png',
|
|
||||||
quality: unknown,
|
|
||||||
) {
|
|
||||||
const dataURL: string = this.toDataURL(type, quality);
|
|
||||||
let data;
|
|
||||||
|
|
||||||
if (dataURL.includes(BASE64_MARKER)) {
|
|
||||||
const [, base64] = dataURL.split(BASE64_MARKER);
|
|
||||||
data = decodeBase64(base64);
|
|
||||||
} else {
|
|
||||||
[, data] = dataURL.split(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(new Blob([data], { type }));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,2 +1 @@
|
||||||
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
|
|
||||||
import 'requestidlecallback';
|
import 'requestidlecallback';
|
||||||
|
|
|
@ -4,39 +4,18 @@
|
||||||
|
|
||||||
import { loadIntlPolyfills } from './intl';
|
import { loadIntlPolyfills } from './intl';
|
||||||
|
|
||||||
function importBasePolyfills() {
|
|
||||||
return import(/* webpackChunkName: "base_polyfills" */ './base_polyfills');
|
|
||||||
}
|
|
||||||
|
|
||||||
function importExtraPolyfills() {
|
function importExtraPolyfills() {
|
||||||
return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills');
|
return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadPolyfills() {
|
export function loadPolyfills() {
|
||||||
const needsBasePolyfills = !(
|
// Safari does not have requestIdleCallback.
|
||||||
'toBlob' in HTMLCanvasElement.prototype &&
|
|
||||||
'assign' in Object &&
|
|
||||||
'values' in Object &&
|
|
||||||
'Symbol' in window &&
|
|
||||||
'finally' in Promise.prototype
|
|
||||||
);
|
|
||||||
|
|
||||||
// Latest version of Firefox and Safari do not have IntersectionObserver.
|
|
||||||
// Edge does not have requestIdleCallback.
|
|
||||||
// This avoids shipping them all the polyfills.
|
// This avoids shipping them all the polyfills.
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types */
|
const needsExtraPolyfills = !window.requestIdleCallback;
|
||||||
const needsExtraPolyfills = !(
|
|
||||||
window.AbortController &&
|
|
||||||
window.IntersectionObserver &&
|
|
||||||
window.IntersectionObserverEntry &&
|
|
||||||
'isIntersecting' in IntersectionObserverEntry.prototype &&
|
|
||||||
window.requestIdleCallback
|
|
||||||
);
|
|
||||||
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
|
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
loadIntlPolyfills(),
|
loadIntlPolyfills(),
|
||||||
needsBasePolyfills && importBasePolyfills(),
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types
|
||||||
needsExtraPolyfills && importExtraPolyfills(),
|
needsExtraPolyfills && importExtraPolyfills(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
|
||||||
|
|
||||||
import { ACCOUNT_REVEAL } from 'mastodon/actions/accounts';
|
|
||||||
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from 'mastodon/actions/importer';
|
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
|
||||||
|
|
||||||
const normalizeAccount = (state, account) => {
|
|
||||||
account = { ...account };
|
|
||||||
|
|
||||||
delete account.followers_count;
|
|
||||||
delete account.following_count;
|
|
||||||
delete account.statuses_count;
|
|
||||||
|
|
||||||
account.hidden = state.getIn([account.id, 'hidden']) === false ? false : account.limited;
|
|
||||||
|
|
||||||
return state.set(account.id, fromJS(account));
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeAccounts = (state, accounts) => {
|
|
||||||
accounts.forEach(account => {
|
|
||||||
state = normalizeAccount(state, account);
|
|
||||||
});
|
|
||||||
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function accounts(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
|
||||||
case ACCOUNT_IMPORT:
|
|
||||||
return normalizeAccount(state, action.account);
|
|
||||||
case ACCOUNTS_IMPORT:
|
|
||||||
return normalizeAccounts(state, action.accounts);
|
|
||||||
case ACCOUNT_REVEAL:
|
|
||||||
return state.setIn([action.id, 'hidden'], false);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
84
app/javascript/mastodon/reducers/accounts.ts
Normal file
84
app/javascript/mastodon/reducers/accounts.ts
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
import type { Reducer } from 'redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
followAccountSuccess,
|
||||||
|
unfollowAccountSuccess,
|
||||||
|
importAccounts,
|
||||||
|
revealAccount,
|
||||||
|
} from 'mastodon/actions/accounts_typed';
|
||||||
|
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||||
|
import { me } from 'mastodon/initial_state';
|
||||||
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
import { createAccountFromServerJSON } from 'mastodon/models/account';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap<string, Account>();
|
||||||
|
|
||||||
|
const normalizeAccount = (
|
||||||
|
state: typeof initialState,
|
||||||
|
account: ApiAccountJSON,
|
||||||
|
) => {
|
||||||
|
return state.set(
|
||||||
|
account.id,
|
||||||
|
createAccountFromServerJSON(account).set(
|
||||||
|
'hidden',
|
||||||
|
state.get(account.id)?.hidden === false
|
||||||
|
? false
|
||||||
|
: account.limited || false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAccounts = (
|
||||||
|
state: typeof initialState,
|
||||||
|
accounts: ApiAccountJSON[],
|
||||||
|
) => {
|
||||||
|
accounts.forEach((account) => {
|
||||||
|
state = normalizeAccount(state, account);
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCurrentUser() {
|
||||||
|
if (!me)
|
||||||
|
throw new Error(
|
||||||
|
'No current user (me) defined when calling `accountsReducer`',
|
||||||
|
);
|
||||||
|
|
||||||
|
return me;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const accountsReducer: Reducer<typeof initialState> = (
|
||||||
|
state = initialState,
|
||||||
|
action,
|
||||||
|
) => {
|
||||||
|
if (revealAccount.match(action))
|
||||||
|
return state.setIn([action.payload.id, 'hidden'], false);
|
||||||
|
else if (importAccounts.match(action))
|
||||||
|
return normalizeAccounts(state, action.payload.accounts);
|
||||||
|
else if (followAccountSuccess.match(action)) {
|
||||||
|
return state
|
||||||
|
.update(
|
||||||
|
action.payload.relationship.id,
|
||||||
|
(account) => account?.update('followers_count', (n) => n + 1),
|
||||||
|
)
|
||||||
|
.update(
|
||||||
|
getCurrentUser(),
|
||||||
|
(account) => account?.update('following_count', (n) => n + 1),
|
||||||
|
);
|
||||||
|
} else if (unfollowAccountSuccess.match(action))
|
||||||
|
return state
|
||||||
|
.update(
|
||||||
|
action.payload.relationship.id,
|
||||||
|
(account) =>
|
||||||
|
account?.update('followers_count', (n) => Math.max(0, n - 1)),
|
||||||
|
)
|
||||||
|
.update(
|
||||||
|
getCurrentUser(),
|
||||||
|
(account) =>
|
||||||
|
account?.update('following_count', (n) => Math.max(0, n - 1)),
|
||||||
|
);
|
||||||
|
else return state;
|
||||||
|
};
|
|
@ -1,49 +0,0 @@
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
|
||||||
|
|
||||||
import { me } from 'mastodon/initial_state';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ACCOUNT_FOLLOW_SUCCESS,
|
|
||||||
ACCOUNT_UNFOLLOW_SUCCESS,
|
|
||||||
} from '../actions/accounts';
|
|
||||||
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
|
|
||||||
|
|
||||||
const normalizeAccount = (state, account) => state.set(account.id, fromJS({
|
|
||||||
followers_count: account.followers_count,
|
|
||||||
following_count: account.following_count,
|
|
||||||
statuses_count: account.statuses_count,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const normalizeAccounts = (state, accounts) => {
|
|
||||||
accounts.forEach(account => {
|
|
||||||
state = normalizeAccount(state, account);
|
|
||||||
});
|
|
||||||
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
|
|
||||||
const incrementFollowers = (state, accountId) =>
|
|
||||||
state.updateIn([accountId, 'followers_count'], num => num + 1)
|
|
||||||
.updateIn([me, 'following_count'], num => num + 1);
|
|
||||||
|
|
||||||
const decrementFollowers = (state, accountId) =>
|
|
||||||
state.updateIn([accountId, 'followers_count'], num => Math.max(0, num - 1))
|
|
||||||
.updateIn([me, 'following_count'], num => Math.max(0, num - 1));
|
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
|
||||||
|
|
||||||
export default function accountsCounters(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
|
||||||
case ACCOUNT_IMPORT:
|
|
||||||
return normalizeAccount(state, action.account);
|
|
||||||
case ACCOUNTS_IMPORT:
|
|
||||||
return normalizeAccounts(state, action.accounts);
|
|
||||||
case ACCOUNT_FOLLOW_SUCCESS:
|
|
||||||
return action.alreadyFollowing ? state :
|
|
||||||
incrementFollowers(state, action.relationship.id);
|
|
||||||
case ACCOUNT_UNFOLLOW_SUCCESS:
|
|
||||||
return decrementFollowers(state, action.relationship.id);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
import { ACCOUNT_LOOKUP_FAIL } from '../actions/accounts';
|
import { ACCOUNT_LOOKUP_FAIL } from '../actions/accounts';
|
||||||
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
|
import { importAccounts } from '../actions/accounts_typed';
|
||||||
|
|
||||||
export const normalizeForLookup = str => str.toLowerCase();
|
export const normalizeForLookup = str => str.toLowerCase();
|
||||||
|
|
||||||
|
@ -11,10 +11,8 @@ export default function accountsMap(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case ACCOUNT_LOOKUP_FAIL:
|
case ACCOUNT_LOOKUP_FAIL:
|
||||||
return action.error?.response?.status === 404 ? state.set(normalizeForLookup(action.acct), null) : state;
|
return action.error?.response?.status === 404 ? state.set(normalizeForLookup(action.acct), null) : state;
|
||||||
case ACCOUNT_IMPORT:
|
case importAccounts.type:
|
||||||
return state.set(normalizeForLookup(action.account.acct), action.account.id);
|
return state.withMutations(map => action.payload.accounts.forEach(account => map.set(normalizeForLookup(account.acct), account.id)));
|
||||||
case ACCOUNTS_IMPORT:
|
|
||||||
return state.withMutations(map => action.accounts.forEach(account => map.set(normalizeForLookup(account.acct), account.id)));
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
blockAccountSuccess,
|
||||||
ACCOUNT_MUTE_SUCCESS,
|
muteAccountSuccess,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
|
import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
|
||||||
import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines';
|
import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines';
|
||||||
|
@ -92,9 +92,9 @@ const updateContext = (state, status) => {
|
||||||
|
|
||||||
export default function replies(state = initialState, action) {
|
export default function replies(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
case blockAccountSuccess.type:
|
||||||
case ACCOUNT_MUTE_SUCCESS:
|
case muteAccountSuccess.type:
|
||||||
return filterContexts(state, action.relationship, action.statuses);
|
return filterContexts(state, action.payload.relationship, action.payload.statuses);
|
||||||
case CONTEXT_FETCH_SUCCESS:
|
case CONTEXT_FETCH_SUCCESS:
|
||||||
return normalizeContext(state, action.id, action.ancestors, action.descendants);
|
return normalizeContext(state, action.id, action.ancestors, action.descendants);
|
||||||
case TIMELINE_DELETE:
|
case TIMELINE_DELETE:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'mastodon/actions/accounts';
|
import { blockAccountSuccess, muteAccountSuccess } from 'mastodon/actions/accounts';
|
||||||
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
|
import { blockDomainSuccess } from 'mastodon/actions/domain_blocks';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CONVERSATIONS_MOUNT,
|
CONVERSATIONS_MOUNT,
|
||||||
|
@ -105,11 +105,11 @@ export default function conversations(state = initialState, action) {
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}));
|
}));
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
case blockAccountSuccess.type:
|
||||||
case ACCOUNT_MUTE_SUCCESS:
|
case muteAccountSuccess.type:
|
||||||
return filterConversations(state, [action.relationship.id]);
|
return filterConversations(state, [action.payload.relationship.id]);
|
||||||
case DOMAIN_BLOCK_SUCCESS:
|
case blockDomainSuccess.type:
|
||||||
return filterConversations(state, action.accounts);
|
return filterConversations(state, action.payload.accounts);
|
||||||
case CONVERSATIONS_DELETE_SUCCESS:
|
case CONVERSATIONS_DELETE_SUCCESS:
|
||||||
return state.update('items', list => list.filterNot(item => item.get('id') === action.id));
|
return state.update('items', list => list.filterNot(item => item.get('id') === action.id));
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutabl
|
||||||
import {
|
import {
|
||||||
DOMAIN_BLOCKS_FETCH_SUCCESS,
|
DOMAIN_BLOCKS_FETCH_SUCCESS,
|
||||||
DOMAIN_BLOCKS_EXPAND_SUCCESS,
|
DOMAIN_BLOCKS_EXPAND_SUCCESS,
|
||||||
DOMAIN_UNBLOCK_SUCCESS,
|
unblockDomainSuccess
|
||||||
} from '../actions/domain_blocks';
|
} from '../actions/domain_blocks';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
|
@ -18,8 +18,8 @@ export default function domainLists(state = initialState, action) {
|
||||||
return state.setIn(['blocks', 'items'], ImmutableOrderedSet(action.domains)).setIn(['blocks', 'next'], action.next);
|
return state.setIn(['blocks', 'items'], ImmutableOrderedSet(action.domains)).setIn(['blocks', 'next'], action.next);
|
||||||
case DOMAIN_BLOCKS_EXPAND_SUCCESS:
|
case DOMAIN_BLOCKS_EXPAND_SUCCESS:
|
||||||
return state.updateIn(['blocks', 'items'], set => set.union(action.domains)).setIn(['blocks', 'next'], action.next);
|
return state.updateIn(['blocks', 'items'], set => set.union(action.domains)).setIn(['blocks', 'next'], action.next);
|
||||||
case DOMAIN_UNBLOCK_SUCCESS:
|
case unblockDomainSuccess.type:
|
||||||
return state.updateIn(['blocks', 'items'], set => set.delete(action.domain));
|
return state.updateIn(['blocks', 'items'], set => set.delete(action.payload.domain));
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,7 @@ import { Record as ImmutableRecord } from 'immutable';
|
||||||
import { loadingBarReducer } from 'react-redux-loading-bar';
|
import { loadingBarReducer } from 'react-redux-loading-bar';
|
||||||
import { combineReducers } from 'redux-immutable';
|
import { combineReducers } from 'redux-immutable';
|
||||||
|
|
||||||
import accounts from './accounts';
|
import { accountsReducer } from './accounts';
|
||||||
import accounts_counters from './accounts_counters';
|
|
||||||
import accounts_map from './accounts_map';
|
import accounts_map from './accounts_map';
|
||||||
import alerts from './alerts';
|
import alerts from './alerts';
|
||||||
import announcements from './announcements';
|
import announcements from './announcements';
|
||||||
|
@ -32,7 +31,7 @@ import notifications from './notifications';
|
||||||
import picture_in_picture from './picture_in_picture';
|
import picture_in_picture from './picture_in_picture';
|
||||||
import polls from './polls';
|
import polls from './polls';
|
||||||
import push_notifications from './push_notifications';
|
import push_notifications from './push_notifications';
|
||||||
import relationships from './relationships';
|
import { relationshipsReducer } from './relationships';
|
||||||
import search from './search';
|
import search from './search';
|
||||||
import server from './server';
|
import server from './server';
|
||||||
import settings from './settings';
|
import settings from './settings';
|
||||||
|
@ -55,11 +54,10 @@ const reducers = {
|
||||||
user_lists,
|
user_lists,
|
||||||
domain_lists,
|
domain_lists,
|
||||||
status_lists,
|
status_lists,
|
||||||
accounts,
|
accounts: accountsReducer,
|
||||||
accounts_counters,
|
|
||||||
accounts_map,
|
accounts_map,
|
||||||
statuses,
|
statuses,
|
||||||
relationships,
|
relationships: relationshipsReducer,
|
||||||
settings,
|
settings,
|
||||||
push_notifications,
|
push_notifications,
|
||||||
mutes,
|
mutes,
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
|
import { blockDomainSuccess } from 'mastodon/actions/domain_blocks';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
authorizeFollowRequestSuccess,
|
||||||
ACCOUNT_MUTE_SUCCESS,
|
blockAccountSuccess,
|
||||||
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
|
muteAccountSuccess,
|
||||||
FOLLOW_REQUEST_REJECT_SUCCESS,
|
rejectFollowRequestSuccess,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import {
|
import {
|
||||||
focusApp,
|
focusApp,
|
||||||
|
@ -16,7 +16,7 @@ import {
|
||||||
MARKERS_FETCH_SUCCESS,
|
MARKERS_FETCH_SUCCESS,
|
||||||
} from '../actions/markers';
|
} from '../actions/markers';
|
||||||
import {
|
import {
|
||||||
NOTIFICATIONS_UPDATE,
|
notificationsUpdate,
|
||||||
NOTIFICATIONS_EXPAND_SUCCESS,
|
NOTIFICATIONS_EXPAND_SUCCESS,
|
||||||
NOTIFICATIONS_EXPAND_REQUEST,
|
NOTIFICATIONS_EXPAND_REQUEST,
|
||||||
NOTIFICATIONS_EXPAND_FAIL,
|
NOTIFICATIONS_EXPAND_FAIL,
|
||||||
|
@ -274,19 +274,19 @@ export default function notifications(state = initialState, action) {
|
||||||
return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', true);
|
return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', true);
|
||||||
case NOTIFICATIONS_SCROLL_TOP:
|
case NOTIFICATIONS_SCROLL_TOP:
|
||||||
return updateTop(state, action.top);
|
return updateTop(state, action.top);
|
||||||
case NOTIFICATIONS_UPDATE:
|
case notificationsUpdate.type:
|
||||||
return normalizeNotification(state, action.notification, action.usePendingItems);
|
return normalizeNotification(state, action.payload.notification, action.payload.usePendingItems);
|
||||||
case NOTIFICATIONS_EXPAND_SUCCESS:
|
case NOTIFICATIONS_EXPAND_SUCCESS:
|
||||||
return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingMore, action.isLoadingRecent, action.usePendingItems);
|
return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingMore, action.isLoadingRecent, action.usePendingItems);
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
case blockAccountSuccess.type:
|
||||||
return filterNotifications(state, [action.relationship.id]);
|
return filterNotifications(state, [action.payload.relationship.id]);
|
||||||
case ACCOUNT_MUTE_SUCCESS:
|
case muteAccountSuccess.type:
|
||||||
return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
|
return action.relationship.muting_notifications ? filterNotifications(state, [action.payload.relationship.id]) : state;
|
||||||
case DOMAIN_BLOCK_SUCCESS:
|
case blockDomainSuccess.type:
|
||||||
return filterNotifications(state, action.accounts);
|
return filterNotifications(state, action.payload.accounts);
|
||||||
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
|
case authorizeFollowRequestSuccess.type:
|
||||||
case FOLLOW_REQUEST_REJECT_SUCCESS:
|
case rejectFollowRequestSuccess.type:
|
||||||
return filterNotifications(state, [action.id], 'follow_request');
|
return filterNotifications(state, [action.payload.id], 'follow_request');
|
||||||
case NOTIFICATIONS_CLEAR:
|
case NOTIFICATIONS_CLEAR:
|
||||||
return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
|
return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
|
||||||
case TIMELINE_DELETE:
|
case TIMELINE_DELETE:
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
|
||||||
|
|
||||||
import {
|
|
||||||
submitAccountNote,
|
|
||||||
} from '../actions/account_notes';
|
|
||||||
import {
|
|
||||||
ACCOUNT_FOLLOW_SUCCESS,
|
|
||||||
ACCOUNT_FOLLOW_REQUEST,
|
|
||||||
ACCOUNT_FOLLOW_FAIL,
|
|
||||||
ACCOUNT_UNFOLLOW_SUCCESS,
|
|
||||||
ACCOUNT_UNFOLLOW_REQUEST,
|
|
||||||
ACCOUNT_UNFOLLOW_FAIL,
|
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
|
||||||
ACCOUNT_UNBLOCK_SUCCESS,
|
|
||||||
ACCOUNT_MUTE_SUCCESS,
|
|
||||||
ACCOUNT_UNMUTE_SUCCESS,
|
|
||||||
ACCOUNT_PIN_SUCCESS,
|
|
||||||
ACCOUNT_UNPIN_SUCCESS,
|
|
||||||
RELATIONSHIPS_FETCH_SUCCESS,
|
|
||||||
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
|
|
||||||
FOLLOW_REQUEST_REJECT_SUCCESS,
|
|
||||||
} from '../actions/accounts';
|
|
||||||
import {
|
|
||||||
DOMAIN_BLOCK_SUCCESS,
|
|
||||||
DOMAIN_UNBLOCK_SUCCESS,
|
|
||||||
} from '../actions/domain_blocks';
|
|
||||||
import {
|
|
||||||
NOTIFICATIONS_UPDATE,
|
|
||||||
} from '../actions/notifications';
|
|
||||||
|
|
||||||
|
|
||||||
const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship));
|
|
||||||
|
|
||||||
const normalizeRelationships = (state, relationships) => {
|
|
||||||
relationships.forEach(relationship => {
|
|
||||||
state = normalizeRelationship(state, relationship);
|
|
||||||
});
|
|
||||||
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setDomainBlocking = (state, accounts, blocking) => {
|
|
||||||
return state.withMutations(map => {
|
|
||||||
accounts.forEach(id => {
|
|
||||||
map.setIn([id, 'domain_blocking'], blocking);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
|
||||||
|
|
||||||
export default function relationships(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
|
||||||
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
|
|
||||||
return state.setIn([action.id, 'followed_by'], true).setIn([action.id, 'requested_by'], false);
|
|
||||||
case FOLLOW_REQUEST_REJECT_SUCCESS:
|
|
||||||
return state.setIn([action.id, 'followed_by'], false).setIn([action.id, 'requested_by'], false);
|
|
||||||
case NOTIFICATIONS_UPDATE:
|
|
||||||
return action.notification.type === 'follow_request' ? state.setIn([action.notification.account.id, 'requested_by'], true) : state;
|
|
||||||
case ACCOUNT_FOLLOW_REQUEST:
|
|
||||||
return state.getIn([action.id, 'following']) ? state : state.setIn([action.id, action.locked ? 'requested' : 'following'], true);
|
|
||||||
case ACCOUNT_FOLLOW_FAIL:
|
|
||||||
return state.setIn([action.id, action.locked ? 'requested' : 'following'], false);
|
|
||||||
case ACCOUNT_UNFOLLOW_REQUEST:
|
|
||||||
return state.setIn([action.id, 'following'], false);
|
|
||||||
case ACCOUNT_UNFOLLOW_FAIL:
|
|
||||||
return state.setIn([action.id, 'following'], true);
|
|
||||||
case ACCOUNT_FOLLOW_SUCCESS:
|
|
||||||
case ACCOUNT_UNFOLLOW_SUCCESS:
|
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
|
||||||
case ACCOUNT_UNBLOCK_SUCCESS:
|
|
||||||
case ACCOUNT_MUTE_SUCCESS:
|
|
||||||
case ACCOUNT_UNMUTE_SUCCESS:
|
|
||||||
case ACCOUNT_PIN_SUCCESS:
|
|
||||||
case ACCOUNT_UNPIN_SUCCESS:
|
|
||||||
return normalizeRelationship(state, action.relationship);
|
|
||||||
case RELATIONSHIPS_FETCH_SUCCESS:
|
|
||||||
return normalizeRelationships(state, action.relationships);
|
|
||||||
case submitAccountNote.fulfilled:
|
|
||||||
return normalizeRelationship(state, action.payload.relationship);
|
|
||||||
case DOMAIN_BLOCK_SUCCESS:
|
|
||||||
return setDomainBlocking(state, action.accounts, true);
|
|
||||||
case DOMAIN_UNBLOCK_SUCCESS:
|
|
||||||
return setDomainBlocking(state, action.accounts, false);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
123
app/javascript/mastodon/reducers/relationships.ts
Normal file
123
app/javascript/mastodon/reducers/relationships.ts
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
import { isFulfilled } from '@reduxjs/toolkit';
|
||||||
|
import type { Reducer } from 'redux';
|
||||||
|
|
||||||
|
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
|
||||||
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
import { createRelationship } from 'mastodon/models/relationship';
|
||||||
|
import type { Relationship } from 'mastodon/models/relationship';
|
||||||
|
|
||||||
|
import { submitAccountNote } from '../actions/account_notes';
|
||||||
|
import {
|
||||||
|
followAccountSuccess,
|
||||||
|
unfollowAccountSuccess,
|
||||||
|
authorizeFollowRequestSuccess,
|
||||||
|
rejectFollowRequestSuccess,
|
||||||
|
followAccountRequest,
|
||||||
|
followAccountFail,
|
||||||
|
unfollowAccountRequest,
|
||||||
|
unfollowAccountFail,
|
||||||
|
blockAccountSuccess,
|
||||||
|
unblockAccountSuccess,
|
||||||
|
muteAccountSuccess,
|
||||||
|
unmuteAccountSuccess,
|
||||||
|
pinAccountSuccess,
|
||||||
|
unpinAccountSuccess,
|
||||||
|
fetchRelationshipsSuccess,
|
||||||
|
} from '../actions/accounts_typed';
|
||||||
|
import {
|
||||||
|
blockDomainSuccess,
|
||||||
|
unblockDomainSuccess,
|
||||||
|
} from '../actions/domain_blocks_typed';
|
||||||
|
import { notificationsUpdate } from '../actions/notifications_typed';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap<string, Relationship>();
|
||||||
|
type State = typeof initialState;
|
||||||
|
|
||||||
|
const normalizeRelationship = (
|
||||||
|
state: State,
|
||||||
|
relationship: ApiRelationshipJSON,
|
||||||
|
) => state.set(relationship.id, createRelationship(relationship));
|
||||||
|
|
||||||
|
const normalizeRelationships = (
|
||||||
|
state: State,
|
||||||
|
relationships: ApiRelationshipJSON[],
|
||||||
|
) => {
|
||||||
|
relationships.forEach((relationship) => {
|
||||||
|
state = normalizeRelationship(state, relationship);
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDomainBlocking = (
|
||||||
|
state: State,
|
||||||
|
accounts: Account[],
|
||||||
|
blocking: boolean,
|
||||||
|
) => {
|
||||||
|
return state.withMutations((map) => {
|
||||||
|
accounts.forEach((id) => {
|
||||||
|
map.setIn([id, 'domain_blocking'], blocking);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const relationshipsReducer: Reducer<State> = (
|
||||||
|
state = initialState,
|
||||||
|
action,
|
||||||
|
) => {
|
||||||
|
if (authorizeFollowRequestSuccess.match(action))
|
||||||
|
return state
|
||||||
|
.setIn([action.payload.id, 'followed_by'], true)
|
||||||
|
.setIn([action.payload.id, 'requested_by'], false);
|
||||||
|
else if (rejectFollowRequestSuccess.match(action))
|
||||||
|
return state
|
||||||
|
.setIn([action.payload.id, 'followed_by'], false)
|
||||||
|
.setIn([action.payload.id, 'requested_by'], false);
|
||||||
|
else if (notificationsUpdate.match(action))
|
||||||
|
return action.payload.notification.type === 'follow_request'
|
||||||
|
? state.setIn(
|
||||||
|
[action.payload.notification.account.id, 'requested_by'],
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
: state;
|
||||||
|
else if (followAccountRequest.match(action))
|
||||||
|
return state.getIn([action.payload.id, 'following'])
|
||||||
|
? state
|
||||||
|
: state.setIn(
|
||||||
|
[
|
||||||
|
action.payload.id,
|
||||||
|
action.payload.locked ? 'requested' : 'following',
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
else if (followAccountFail.match(action))
|
||||||
|
return state.setIn(
|
||||||
|
[action.payload.id, action.payload.locked ? 'requested' : 'following'],
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
else if (unfollowAccountRequest.match(action))
|
||||||
|
return state.setIn([action.payload.id, 'following'], false);
|
||||||
|
else if (unfollowAccountFail.match(action))
|
||||||
|
return state.setIn([action.payload.id, 'following'], true);
|
||||||
|
else if (
|
||||||
|
followAccountSuccess.match(action) ||
|
||||||
|
unfollowAccountSuccess.match(action) ||
|
||||||
|
blockAccountSuccess.match(action) ||
|
||||||
|
unblockAccountSuccess.match(action) ||
|
||||||
|
muteAccountSuccess.match(action) ||
|
||||||
|
unmuteAccountSuccess.match(action) ||
|
||||||
|
pinAccountSuccess.match(action) ||
|
||||||
|
unpinAccountSuccess.match(action) ||
|
||||||
|
isFulfilled(submitAccountNote)(action)
|
||||||
|
)
|
||||||
|
return normalizeRelationship(state, action.payload.relationship);
|
||||||
|
else if (fetchRelationshipsSuccess.match(action))
|
||||||
|
return normalizeRelationships(state, action.payload.relationships);
|
||||||
|
else if (blockDomainSuccess.match(action))
|
||||||
|
return setDomainBlocking(state, action.payload.accounts, true);
|
||||||
|
else if (unblockDomainSuccess.match(action))
|
||||||
|
return setDomainBlocking(state, action.payload.accounts, false);
|
||||||
|
else return state;
|
||||||
|
};
|
|
@ -1,8 +1,8 @@
|
||||||
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
blockAccountSuccess,
|
||||||
ACCOUNT_MUTE_SUCCESS,
|
muteAccountSuccess,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import {
|
import {
|
||||||
BOOKMARKED_STATUSES_FETCH_REQUEST,
|
BOOKMARKED_STATUSES_FETCH_REQUEST,
|
||||||
|
@ -142,9 +142,9 @@ export default function statusLists(state = initialState, action) {
|
||||||
return prependOneToList(state, 'pins', action.status);
|
return prependOneToList(state, 'pins', action.status);
|
||||||
case UNPIN_SUCCESS:
|
case UNPIN_SUCCESS:
|
||||||
return removeOneFromList(state, 'pins', action.status);
|
return removeOneFromList(state, 'pins', action.status);
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
case blockAccountSuccess.type:
|
||||||
case ACCOUNT_MUTE_SUCCESS:
|
case muteAccountSuccess.type:
|
||||||
return state.updateIn(['trending', 'items'], ImmutableOrderedSet(), list => list.filterNot(statusId => action.statuses.getIn([statusId, 'account']) === action.relationship.id));
|
return state.updateIn(['trending', 'items'], ImmutableOrderedSet(), list => list.filterNot(statusId => action.payload.statuses.getIn([statusId, 'account']) === action.payload.relationship.id));
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||||
|
|
||||||
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'mastodon/actions/accounts';
|
import { blockAccountSuccess, muteAccountSuccess } from 'mastodon/actions/accounts';
|
||||||
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
|
import { blockDomainSuccess } from 'mastodon/actions/domain_blocks';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SUGGESTIONS_FETCH_REQUEST,
|
SUGGESTIONS_FETCH_REQUEST,
|
||||||
|
@ -29,11 +29,11 @@ export default function suggestionsReducer(state = initialState, action) {
|
||||||
return state.set('isLoading', false);
|
return state.set('isLoading', false);
|
||||||
case SUGGESTIONS_DISMISS:
|
case SUGGESTIONS_DISMISS:
|
||||||
return state.update('items', list => list.filterNot(x => x.account === action.id));
|
return state.update('items', list => list.filterNot(x => x.account === action.id));
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
case blockAccountSuccess.type:
|
||||||
case ACCOUNT_MUTE_SUCCESS:
|
case muteAccountSuccess.type:
|
||||||
return state.update('items', list => list.filterNot(x => x.account === action.relationship.id));
|
return state.update('items', list => list.filterNot(x => x.account === action.payload.relationship.id));
|
||||||
case DOMAIN_BLOCK_SUCCESS:
|
case blockDomainSuccess.type:
|
||||||
return state.update('items', list => list.filterNot(x => action.accounts.includes(x.account)));
|
return state.update('items', list => list.filterNot(x => action.payload.accounts.includes(x.account)));
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
blockAccountSuccess,
|
||||||
ACCOUNT_MUTE_SUCCESS,
|
muteAccountSuccess,
|
||||||
ACCOUNT_UNFOLLOW_SUCCESS,
|
unfollowAccountSuccess
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import {
|
import {
|
||||||
TIMELINE_UPDATE,
|
TIMELINE_UPDATE,
|
||||||
|
@ -200,11 +200,11 @@ export default function timelines(state = initialState, action) {
|
||||||
return deleteStatus(state, action.id, action.references, action.reblogOf);
|
return deleteStatus(state, action.id, action.references, action.reblogOf);
|
||||||
case TIMELINE_CLEAR:
|
case TIMELINE_CLEAR:
|
||||||
return clearTimeline(state, action.timeline);
|
return clearTimeline(state, action.timeline);
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
case blockAccountSuccess.type:
|
||||||
case ACCOUNT_MUTE_SUCCESS:
|
case muteAccountSuccess.type:
|
||||||
return filterTimelines(state, action.relationship, action.statuses);
|
return filterTimelines(state, action.payload.relationship, action.payload.statuses);
|
||||||
case ACCOUNT_UNFOLLOW_SUCCESS:
|
case unfollowAccountSuccess.type:
|
||||||
return filterTimeline('home', state, action.relationship, action.statuses);
|
return filterTimeline('home', state, action.payload.relationship, action.payload.statuses);
|
||||||
case TIMELINE_SCROLL_TOP:
|
case TIMELINE_SCROLL_TOP:
|
||||||
return updateTop(state, action.timeline, action.top);
|
return updateTop(state, action.timeline, action.top);
|
||||||
case TIMELINE_CONNECT:
|
case TIMELINE_CONNECT:
|
||||||
|
|
|
@ -33,8 +33,8 @@ import {
|
||||||
FOLLOW_REQUESTS_EXPAND_REQUEST,
|
FOLLOW_REQUESTS_EXPAND_REQUEST,
|
||||||
FOLLOW_REQUESTS_EXPAND_SUCCESS,
|
FOLLOW_REQUESTS_EXPAND_SUCCESS,
|
||||||
FOLLOW_REQUESTS_EXPAND_FAIL,
|
FOLLOW_REQUESTS_EXPAND_FAIL,
|
||||||
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
|
authorizeFollowRequestSuccess,
|
||||||
FOLLOW_REQUEST_REJECT_SUCCESS,
|
rejectFollowRequestSuccess,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import {
|
import {
|
||||||
BLOCKS_FETCH_REQUEST,
|
BLOCKS_FETCH_REQUEST,
|
||||||
|
@ -66,11 +66,7 @@ import {
|
||||||
MUTES_EXPAND_SUCCESS,
|
MUTES_EXPAND_SUCCESS,
|
||||||
MUTES_EXPAND_FAIL,
|
MUTES_EXPAND_FAIL,
|
||||||
} from '../actions/mutes';
|
} from '../actions/mutes';
|
||||||
import {
|
import { notificationsUpdate } from '../actions/notifications';
|
||||||
NOTIFICATIONS_UPDATE,
|
|
||||||
} from '../actions/notifications';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const initialListState = ImmutableMap({
|
const initialListState = ImmutableMap({
|
||||||
next: null,
|
next: null,
|
||||||
|
@ -163,8 +159,8 @@ export default function userLists(state = initialState, action) {
|
||||||
case FAVOURITES_FETCH_FAIL:
|
case FAVOURITES_FETCH_FAIL:
|
||||||
case FAVOURITES_EXPAND_FAIL:
|
case FAVOURITES_EXPAND_FAIL:
|
||||||
return state.setIn(['favourited_by', action.id, 'isLoading'], false);
|
return state.setIn(['favourited_by', action.id, 'isLoading'], false);
|
||||||
case NOTIFICATIONS_UPDATE:
|
case notificationsUpdate.type:
|
||||||
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
|
return action.payload.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.payload.notification) : state;
|
||||||
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
||||||
return normalizeList(state, ['follow_requests'], action.accounts, action.next);
|
return normalizeList(state, ['follow_requests'], action.accounts, action.next);
|
||||||
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
||||||
|
@ -175,9 +171,9 @@ export default function userLists(state = initialState, action) {
|
||||||
case FOLLOW_REQUESTS_FETCH_FAIL:
|
case FOLLOW_REQUESTS_FETCH_FAIL:
|
||||||
case FOLLOW_REQUESTS_EXPAND_FAIL:
|
case FOLLOW_REQUESTS_EXPAND_FAIL:
|
||||||
return state.setIn(['follow_requests', 'isLoading'], false);
|
return state.setIn(['follow_requests', 'isLoading'], false);
|
||||||
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
|
case authorizeFollowRequestSuccess.type:
|
||||||
case FOLLOW_REQUEST_REJECT_SUCCESS:
|
case rejectFollowRequestSuccess.type:
|
||||||
return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id));
|
return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.payload.id));
|
||||||
case BLOCKS_FETCH_SUCCESS:
|
case BLOCKS_FETCH_SUCCESS:
|
||||||
return normalizeList(state, ['blocks'], action.accounts, action.next);
|
return normalizeList(state, ['blocks'], action.accounts, action.next);
|
||||||
case BLOCKS_EXPAND_SUCCESS:
|
case BLOCKS_EXPAND_SUCCESS:
|
||||||
|
|
47
app/javascript/mastodon/selectors/accounts.ts
Normal file
47
app/javascript/mastodon/selectors/accounts.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { Record as ImmutableRecord } from 'immutable';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { accountDefaultValues } from 'mastodon/models/account';
|
||||||
|
import type { Account, AccountShape } from 'mastodon/models/account';
|
||||||
|
import type { Relationship } from 'mastodon/models/relationship';
|
||||||
|
import type { RootState } from 'mastodon/store';
|
||||||
|
|
||||||
|
const getAccountBase = (state: RootState, id: string) =>
|
||||||
|
state.accounts.get(id, null);
|
||||||
|
|
||||||
|
const getAccountRelationship = (state: RootState, id: string) =>
|
||||||
|
state.relationships.get(id, null);
|
||||||
|
|
||||||
|
const getAccountMoved = (state: RootState, id: string) => {
|
||||||
|
const movedToId = state.accounts.get(id)?.moved;
|
||||||
|
|
||||||
|
if (!movedToId) return undefined;
|
||||||
|
|
||||||
|
return state.accounts.get(movedToId);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FullAccountShape extends Omit<AccountShape, 'moved'> {
|
||||||
|
relationship: Relationship | null;
|
||||||
|
moved: Account | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FullAccountFactory = ImmutableRecord<FullAccountShape>({
|
||||||
|
...accountDefaultValues,
|
||||||
|
moved: null,
|
||||||
|
relationship: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function makeGetAccount() {
|
||||||
|
return createSelector(
|
||||||
|
[getAccountBase, getAccountRelationship, getAccountMoved],
|
||||||
|
(base, relationship, moved) => {
|
||||||
|
if (base === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FullAccountFactory(base)
|
||||||
|
.set('relationship', relationship)
|
||||||
|
.set('moved', moved ?? null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
|
@ -5,23 +5,7 @@ import { toServerSideType } from 'mastodon/utils/filters';
|
||||||
|
|
||||||
import { me } from '../initial_state';
|
import { me } from '../initial_state';
|
||||||
|
|
||||||
const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
|
export { makeGetAccount } from "./accounts";
|
||||||
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
|
|
||||||
const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null);
|
|
||||||
const getAccountMoved = (state, id) => state.getIn(['accounts', state.getIn(['accounts', id, 'moved'])]);
|
|
||||||
|
|
||||||
export const makeGetAccount = () => {
|
|
||||||
return createSelector([getAccountBase, getAccountCounters, getAccountRelationship, getAccountMoved], (base, counters, relationship, moved) => {
|
|
||||||
if (base === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.merge(counters).withMutations(map => {
|
|
||||||
map.set('relationship', relationship);
|
|
||||||
map.set('moved', moved);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFilters = (state, { contextType }) => {
|
const getFilters = (state, { contextType }) => {
|
||||||
if (!contextType) return null;
|
if (!contextType) return null;
|
||||||
|
|
|
@ -35,6 +35,5 @@ export const store = configureStore({
|
||||||
|
|
||||||
// Infer the `RootState` and `AppDispatch` types from the store itself
|
// Infer the `RootState` and `AppDispatch` types from the store itself
|
||||||
export type RootState = ReturnType<typeof rootReducer>;
|
export type RootState = ReturnType<typeof rootReducer>;
|
||||||
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
|
||||||
export type AppDispatch = typeof store.dispatch;
|
export type AppDispatch = typeof store.dispatch;
|
||||||
export type GetState = typeof store.getState;
|
export type GetState = typeof store.getState;
|
||||||
|
|
7
app/javascript/mastodon/utils/environment.ts
Normal file
7
app/javascript/mastodon/utils/environment.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export function isDevelopment() {
|
||||||
|
return process.env.NODE_ENV === 'development';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isProduction() {
|
||||||
|
return process.env.NODE_ENV === 'production';
|
||||||
|
}
|
|
@ -1,55 +0,0 @@
|
||||||
import type { Record } from 'immutable';
|
|
||||||
|
|
||||||
type CustomEmoji = Record<{
|
|
||||||
shortcode: string;
|
|
||||||
static_url: string;
|
|
||||||
url: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
type AccountField = Record<{
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
verified_at: string | null;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
interface AccountApiResponseValues {
|
|
||||||
acct: string;
|
|
||||||
avatar: string;
|
|
||||||
avatar_static: string;
|
|
||||||
bot: boolean;
|
|
||||||
created_at: string;
|
|
||||||
discoverable: boolean;
|
|
||||||
display_name: string;
|
|
||||||
emojis: CustomEmoji[];
|
|
||||||
fields: AccountField[];
|
|
||||||
followers_count: number;
|
|
||||||
following_count: number;
|
|
||||||
group: boolean;
|
|
||||||
header: string;
|
|
||||||
header_static: string;
|
|
||||||
id: string;
|
|
||||||
last_status_at: string;
|
|
||||||
locked: boolean;
|
|
||||||
note: string;
|
|
||||||
statuses_count: number;
|
|
||||||
url: string;
|
|
||||||
uri: string;
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type NormalizedAccountField = Record<{
|
|
||||||
name_emojified: string;
|
|
||||||
value_emojified: string;
|
|
||||||
value_plain: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
interface NormalizedAccountValues {
|
|
||||||
display_name_html: string;
|
|
||||||
fields: NormalizedAccountField[];
|
|
||||||
note_emojified: string;
|
|
||||||
note_plain: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Account = Record<
|
|
||||||
AccountApiResponseValues & NormalizedAccountValues
|
|
||||||
>;
|
|
|
@ -53,7 +53,8 @@ class ActivityPub::Parser::StatusParser
|
||||||
end
|
end
|
||||||
|
|
||||||
def created_at
|
def created_at
|
||||||
@object['published']&.to_datetime
|
datetime = @object['published']&.to_datetime
|
||||||
|
datetime if datetime.present? && (0..9999).cover?(datetime.year)
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,7 @@ class FanOutOnWriteService < BaseService
|
||||||
# @param [Hash] options
|
# @param [Hash] options
|
||||||
# @option options [Boolean] update
|
# @option options [Boolean] update
|
||||||
# @option options [Array<Integer>] silenced_account_ids
|
# @option options [Array<Integer>] silenced_account_ids
|
||||||
|
# @option options [Boolean] skip_notifications
|
||||||
def call(status, options = {})
|
def call(status, options = {})
|
||||||
@status = status
|
@status = status
|
||||||
@account = status.account
|
@account = status.account
|
||||||
|
@ -37,8 +38,11 @@ class FanOutOnWriteService < BaseService
|
||||||
|
|
||||||
def fan_out_to_local_recipients!
|
def fan_out_to_local_recipients!
|
||||||
deliver_to_self!
|
deliver_to_self!
|
||||||
|
|
||||||
|
unless @options[:skip_notifications]
|
||||||
notify_mentioned_accounts!
|
notify_mentioned_accounts!
|
||||||
notify_about_update! if update?
|
notify_about_update! if update?
|
||||||
|
end
|
||||||
|
|
||||||
case @status.visibility.to_sym
|
case @status.visibility.to_sym
|
||||||
when :public, :unlisted, :private
|
when :public, :unlisted, :private
|
||||||
|
|
|
@ -8,12 +8,17 @@ class ThreadResolveWorker
|
||||||
|
|
||||||
def perform(child_status_id, parent_url, options = {})
|
def perform(child_status_id, parent_url, options = {})
|
||||||
child_status = Status.find(child_status_id)
|
child_status = Status.find(child_status_id)
|
||||||
parent_status = FetchRemoteStatusService.new.call(parent_url, **options.deep_symbolize_keys)
|
return if child_status.in_reply_to_id.present?
|
||||||
|
|
||||||
|
parent_status = ActivityPub::TagManager.instance.uri_to_resource(parent_url, Status)
|
||||||
|
parent_status ||= FetchRemoteStatusService.new.call(parent_url, **options.deep_symbolize_keys)
|
||||||
|
|
||||||
return if parent_status.nil?
|
return if parent_status.nil?
|
||||||
|
|
||||||
child_status.thread = parent_status
|
child_status.thread = parent_status
|
||||||
child_status.save!
|
child_status.save!
|
||||||
|
|
||||||
|
DistributionWorker.perform_async(child_status_id, { 'skip_notifications' => true }) if child_status.within_realtime_window?
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,8 +7,8 @@ module.exports = (api) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const envOptions = {
|
const envOptions = {
|
||||||
loose: true,
|
useBuiltIns: "usage",
|
||||||
modules: false,
|
corejs: { version: "3.30" },
|
||||||
debug: false,
|
debug: false,
|
||||||
include: [
|
include: [
|
||||||
'transform-numeric-separator',
|
'transform-numeric-separator',
|
||||||
|
@ -18,29 +18,14 @@ module.exports = (api) => {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = {
|
const plugins = [
|
||||||
presets: [
|
|
||||||
'@babel/preset-typescript',
|
|
||||||
['@babel/react', reactOptions],
|
|
||||||
['@babel/env', envOptions],
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
['formatjs'],
|
['formatjs'],
|
||||||
'preval',
|
'preval',
|
||||||
],
|
];
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
test: /tesseract\.js/,
|
|
||||||
presets: [
|
|
||||||
['@babel/env', { ...envOptions, modules: 'commonjs' }],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (env) {
|
switch (env) {
|
||||||
case 'production':
|
case 'production':
|
||||||
config.plugins.push(...[
|
plugins.push(...[
|
||||||
'lodash',
|
'lodash',
|
||||||
[
|
[
|
||||||
'transform-react-remove-prop-types',
|
'transform-react-remove-prop-types',
|
||||||
|
@ -63,14 +48,33 @@ module.exports = (api) => {
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'development':
|
case 'development':
|
||||||
reactOptions.development = true;
|
reactOptions.development = true;
|
||||||
envOptions.debug = true;
|
envOptions.debug = true;
|
||||||
break;
|
|
||||||
case 'test':
|
// We need Babel to not inject polyfills in dev, as this breaks `preval` files
|
||||||
envOptions.modules = 'commonjs';
|
envOptions.useBuiltIns = false;
|
||||||
|
envOptions.corejs = undefined;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
presets: [
|
||||||
|
'@babel/preset-typescript',
|
||||||
|
['@babel/react', reactOptions],
|
||||||
|
['@babel/env', envOptions],
|
||||||
|
],
|
||||||
|
plugins,
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
test: /tesseract\.js/,
|
||||||
|
presets: [
|
||||||
|
['@babel/env', { ...envOptions, modules: 'commonjs' }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,15 @@
|
||||||
|
|
||||||
require 'devise/strategies/authenticatable'
|
require 'devise/strategies/authenticatable'
|
||||||
|
|
||||||
|
# TODO: Remove this patch when this PR or similar is merged into Devise:
|
||||||
|
# https://github.com/heartcombo/devise/pull/5645
|
||||||
|
# We rely on ENV vars and not secrets/credentials, so the deprecation is just noise.
|
||||||
|
class Devise::SecretKeyFinder
|
||||||
|
def find
|
||||||
|
@application.secret_key_base
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
Warden::Manager.after_set_user except: :fetch do |user, warden|
|
Warden::Manager.after_set_user except: :fetch do |user, warden|
|
||||||
session_id = warden.cookies.signed['_session_id'] || warden.raw_session['auth_id']
|
session_id = warden.cookies.signed['_session_id'] || warden.raw_session['auth_id']
|
||||||
session_id = user.activate_session(warden.request) unless user.session_activations.active?(session_id)
|
session_id = user.activate_session(warden.request) unless user.session_activations.active?(session_id)
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require_relative '../../lib/json_ld/security'
|
|
||||||
require_relative '../../lib/json_ld/identity'
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue