diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml
index 8dcab845ee085c..25615b720d45ad 100644
--- a/.github/workflows/lint-haml.yml
+++ b/.github/workflows/lint-haml.yml
@@ -36,4 +36,4 @@ jobs:
- name: Run haml-lint
run: |
echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json"
- bundle exec haml-lint
+ bundle exec haml-lint --reporter github
diff --git a/Gemfile.lock b/Gemfile.lock
index 734ed6ec8a19d4..076cf915d05bfd 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -333,7 +333,7 @@ GEM
http-form_data (2.3.0)
http_accept_language (2.1.1)
httpclient (2.8.3)
- httplog (1.6.2)
+ httplog (1.6.3)
rack (>= 2.0)
rainbow (>= 2.0.0)
i18n (1.14.1)
@@ -744,7 +744,7 @@ GEM
terrapin (1.0.1)
climate_control
test-prof (1.3.1)
- thor (1.3.0)
+ thor (1.3.1)
tilt (2.3.0)
timeout (0.4.1)
tpm-key_attestation (0.12.0)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 00f5c7c11e0223..490a45e018bf82 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -180,7 +180,7 @@ def check_self_destruct!
use_pack 'error'
render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html]
end
- format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: code }
+ format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: 410 }
end
end
diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js
index d1c75726404cf5..655133290acb2a 100644
--- a/app/javascript/core/admin.js
+++ b/app/javascript/core/admin.js
@@ -93,18 +93,6 @@ Rails.delegate(document, batchCheckboxClassName, 'change', () => {
}
});
-Rails.delegate(document, '.media-spoiler-show-button', 'click', () => {
- [].forEach.call(document.querySelectorAll('button.media-spoiler'), (element) => {
- element.click();
- });
-});
-
-Rails.delegate(document, '.media-spoiler-hide-button', 'click', () => {
- [].forEach.call(document.querySelectorAll('.spoiler-button.spoiler-button--visible button'), (element) => {
- element.click();
- });
-});
-
Rails.delegate(document, '.filter-subset--with-select select', 'change', ({ target }) => {
target.form.submit();
});
diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_icon_button.jsx b/app/javascript/flavours/glitch/features/compose/components/dropdown_icon_button.jsx
index 0ced5a04ad0d14..9774d4260eb462 100644
--- a/app/javascript/flavours/glitch/features/compose/components/dropdown_icon_button.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_icon_button.jsx
@@ -5,7 +5,7 @@ import Overlay from 'react-overlays/Overlay';
import { IconButton } from 'flavours/glitch/components/icon_button';
-import DropdownMenu from './dropdown_menu';
+import { PrivacyDropdownMenu } from './privacy_dropdown_menu';
export const DropdownIconButton = ({ value, disabled, icon, onChange, iconComponent, title, options }) => {
const containerRef = useRef(null);
@@ -53,7 +53,7 @@ export const DropdownIconButton = ({ value, disabled, icon, onChange, iconCompon
{({ props, placement }) => (
-
{
- if (this.node && !this.node.contains(e.target)) {
- this.props.onClose();
- e.stopPropagation();
- }
- };
-
- handleKeyDown = e => {
- const { items } = this.props;
- const value = e.currentTarget.getAttribute('data-index');
- const index = items.findIndex(item => {
- return (item.value === value);
- });
- let element = null;
-
- switch(e.key) {
- case 'Escape':
- this.props.onClose();
- break;
- case 'Enter':
- this.handleClick(e);
- break;
- case 'ArrowDown':
- element = this.node.childNodes[index + 1] || this.node.firstChild;
- break;
- case 'ArrowUp':
- element = this.node.childNodes[index - 1] || this.node.lastChild;
- break;
- case 'Tab':
- if (e.shiftKey) {
- element = this.node.childNodes[index - 1] || this.node.lastChild;
- } else {
- element = this.node.childNodes[index + 1] || this.node.firstChild;
- }
- break;
- case 'Home':
- element = this.node.firstChild;
- break;
- case 'End':
- element = this.node.lastChild;
- break;
- }
-
- if (element) {
- element.focus();
- this.props.onChange(element.getAttribute('data-index'));
- e.preventDefault();
- e.stopPropagation();
- }
- };
-
- handleClick = e => {
- const value = e.currentTarget.getAttribute('data-index');
-
- e.preventDefault();
-
- this.props.onClose();
- this.props.onChange(value);
- };
-
- componentDidMount () {
- document.addEventListener('click', this.handleDocumentClick, { capture: true });
- document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
- if (this.focusedItem) this.focusedItem.focus({ preventScroll: true });
- }
-
- componentWillUnmount () {
- document.removeEventListener('click', this.handleDocumentClick, { capture: true });
- document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
- }
-
- setRef = c => {
- this.node = c;
- };
-
- setFocusRef = c => {
- this.focusedItem = c;
- };
-
- render () {
- const { style, items, value } = this.props;
-
- return (
-
- {items.map(item => (
-
-
-
-
-
-
- {item.text}
- {item.meta}
-
-
- ))}
-
- );
- }
-
-}
-
-export default DropdownMenu;
diff --git a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx
index db1ce9cecec561..8edf75203facec 100644
--- a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx
@@ -141,6 +141,7 @@ class LanguageDropdownMenu extends PureComponent {
case 'Escape':
onClose();
break;
+ case ' ':
case 'Enter':
this.handleClick(e);
break;
diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx
index 8a49f71511e008..c99f18545ba605 100644
--- a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx
@@ -5,16 +5,16 @@ import { injectIntl, defineMessages } from 'react-intl';
import classNames from 'classnames';
-import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
-import InfoIcon from '@/material-icons/400-24px/info.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
+import { PrivacyDropdownMenu } from './privacy_dropdown_menu';
+
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' },
@@ -28,126 +28,6 @@ const messages = defineMessages({
unlisted_extra: { id: 'privacy.unlisted.additional', defaultMessage: 'This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.' },
});
-const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
-
-class PrivacyDropdownMenu extends PureComponent {
-
- static propTypes = {
- style: PropTypes.object,
- items: PropTypes.array.isRequired,
- value: PropTypes.string.isRequired,
- onClose: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
- };
-
- handleDocumentClick = e => {
- if (this.node && !this.node.contains(e.target)) {
- this.props.onClose();
- e.stopPropagation();
- }
- };
-
- handleKeyDown = e => {
- const { items } = this.props;
- const value = e.currentTarget.getAttribute('data-index');
- const index = items.findIndex(item => {
- return (item.value === value);
- });
- let element = null;
-
- switch(e.key) {
- case 'Escape':
- this.props.onClose();
- break;
- case 'Enter':
- this.handleClick(e);
- break;
- case 'ArrowDown':
- element = this.node.childNodes[index + 1] || this.node.firstChild;
- break;
- case 'ArrowUp':
- element = this.node.childNodes[index - 1] || this.node.lastChild;
- break;
- case 'Tab':
- if (e.shiftKey) {
- element = this.node.childNodes[index - 1] || this.node.lastChild;
- } else {
- element = this.node.childNodes[index + 1] || this.node.firstChild;
- }
- break;
- case 'Home':
- element = this.node.firstChild;
- break;
- case 'End':
- element = this.node.lastChild;
- break;
- }
-
- if (element) {
- element.focus();
- this.props.onChange(element.getAttribute('data-index'));
- e.preventDefault();
- e.stopPropagation();
- }
- };
-
- handleClick = e => {
- const value = e.currentTarget.getAttribute('data-index');
-
- e.preventDefault();
-
- this.props.onClose();
- this.props.onChange(value);
- };
-
- componentDidMount () {
- document.addEventListener('click', this.handleDocumentClick, { capture: true });
- document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
- if (this.focusedItem) this.focusedItem.focus({ preventScroll: true });
- }
-
- componentWillUnmount () {
- document.removeEventListener('click', this.handleDocumentClick, { capture: true });
- document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
- }
-
- setRef = c => {
- this.node = c;
- };
-
- setFocusRef = c => {
- this.focusedItem = c;
- };
-
- render () {
- const { style, items, value } = this.props;
-
- return (
-
- {items.map(item => (
-
-
-
-
-
-
- {item.text}
- {item.meta}
-
-
- {item.extra && (
-
-
-
- )}
-
- ))}
-
- );
- }
-
-}
-
class PrivacyDropdown extends PureComponent {
static propTypes = {
diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown_menu.jsx b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown_menu.jsx
new file mode 100644
index 00000000000000..03a0b76d23914d
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown_menu.jsx
@@ -0,0 +1,128 @@
+import PropTypes from 'prop-types';
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+import classNames from 'classnames';
+
+import { supportsPassiveEvents } from 'detect-passive-events';
+
+import InfoIcon from '@/material-icons/400-24px/info.svg?react';
+import { Icon } from 'flavours/glitch/components/icon';
+
+const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
+
+export const PrivacyDropdownMenu = ({ style, items, value, onClose, onChange }) => {
+ const nodeRef = useRef(null);
+ const focusedItemRef = useRef(null);
+ const [currentValue, setCurrentValue] = useState(value);
+
+ const handleDocumentClick = useCallback((e) => {
+ if (nodeRef.current && !nodeRef.current.contains(e.target)) {
+ onClose();
+ e.stopPropagation();
+ }
+ }, [nodeRef, onClose]);
+
+ const handleClick = useCallback((e) => {
+ const value = e.currentTarget.getAttribute('data-index');
+
+ e.preventDefault();
+
+ onClose();
+ onChange(value);
+ }, [onClose, onChange]);
+
+ const handleKeyDown = useCallback((e) => {
+ const value = e.currentTarget.getAttribute('data-index');
+ const index = items.findIndex(item => (item.value === value));
+
+ let element = null;
+
+ switch (e.key) {
+ case 'Escape':
+ onClose();
+ break;
+ case ' ':
+ case 'Enter':
+ handleClick(e);
+ break;
+ case 'ArrowDown':
+ element = nodeRef.current.childNodes[index + 1] || nodeRef.current.firstChild;
+ break;
+ case 'ArrowUp':
+ element = nodeRef.current.childNodes[index - 1] || nodeRef.current.lastChild;
+ break;
+ case 'Tab':
+ if (e.shiftKey) {
+ element = nodeRef.current.childNodes[index + 1] || nodeRef.current.firstChild;
+ } else {
+ element = nodeRef.current.childNodes[index - 1] || nodeRef.current.lastChild;
+ }
+ break;
+ case 'Home':
+ element = nodeRef.current.firstChild;
+ break;
+ case 'End':
+ element = nodeRef.current.lastChild;
+ break;
+ }
+
+ if (element) {
+ element.focus();
+ setCurrentValue(element.getAttribute('data-index'));
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }, [nodeRef, items, onClose, handleClick, setCurrentValue]);
+
+ useEffect(() => {
+ document.addEventListener('click', handleDocumentClick, { capture: true });
+ document.addEventListener('touchend', handleDocumentClick, listenerOptions);
+ focusedItemRef.current?.focus({ preventScroll: true });
+
+ return () => {
+ document.removeEventListener('click', handleDocumentClick, { capture: true });
+ document.removeEventListener('touchend', handleDocumentClick, listenerOptions);
+ };
+ }, [handleDocumentClick]);
+
+ return (
+
+ );
+};
+
+PrivacyDropdownMenu.propTypes = {
+ style: PropTypes.object,
+ items: PropTypes.array.isRequired,
+ value: PropTypes.string.isRequired,
+ onClose: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+};
diff --git a/app/javascript/flavours/glitch/features/compose/components/thread_mode_button.jsx b/app/javascript/flavours/glitch/features/compose/components/thread_mode_button.jsx
index 9b8947c447b505..59d569a999dd09 100644
--- a/app/javascript/flavours/glitch/features/compose/components/thread_mode_button.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/thread_mode_button.jsx
@@ -8,8 +8,8 @@ import { IconButton } from 'flavours/glitch/components/icon_button';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
const messages = defineMessages({
- enable_threaded_mode: { id: 'compose.enable_threaded_mode', defaultMessage: 'Enable threaded more' },
- disable_threaded_mode: { id: 'compose.disable_threaded_mode', defaultMessage: 'Disable threaded more' },
+ enable_threaded_mode: { id: 'compose.enable_threaded_mode', defaultMessage: 'Enable threaded mode' },
+ disable_threaded_mode: { id: 'compose.disable_threaded_mode', defaultMessage: 'Disable threaded mode' },
});
export const ThreadModeButton = () => {
diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_button.jsx b/app/javascript/flavours/glitch/features/compose/components/upload_button.jsx
index caa6784cb9a420..ed2cbb04f27223 100644
--- a/app/javascript/flavours/glitch/features/compose/components/upload_button.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/upload_button.jsx
@@ -7,10 +7,14 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import PhotoLibraryIcon from '@/material-icons/400-20px/photo_library.svg?react';
-import { IconButton } from 'flavours/glitch/components/icon_button';
+import BrushIcon from '@/material-icons/400-24px/brush.svg?react';
+import UploadFileIcon from '@/material-icons/400-24px/upload_file.svg?react';
+
+import { DropdownIconButton } from './dropdown_icon_button';
const messages = defineMessages({
upload: { id: 'upload_button.label', defaultMessage: 'Add images, a video or an audio file' },
+ doodle: { id: 'compose.attach.doodle', defaultMessage: 'Draw something' },
});
const makeMapStateToProps = () => {
@@ -21,16 +25,12 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
-const iconStyle = {
- height: null,
- lineHeight: '27px',
-};
-
class UploadButton extends ImmutablePureComponent {
static propTypes = {
disabled: PropTypes.bool,
onSelectFile: PropTypes.func.isRequired,
+ onDoodleOpen: PropTypes.func.isRequired,
style: PropTypes.object,
resetFileKey: PropTypes.number,
acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired,
@@ -43,8 +43,12 @@ class UploadButton extends ImmutablePureComponent {
}
};
- handleClick = () => {
- this.fileElement.click();
+ handleSelect = (value) => {
+ if (value === 'upload') {
+ this.fileElement.click();
+ } else {
+ this.props.onDoodleOpen();
+ }
};
setRef = (c) => {
@@ -56,9 +60,32 @@ class UploadButton extends ImmutablePureComponent {
const message = intl.formatMessage(messages.upload);
+ const options = [
+ {
+ icon: 'cloud-upload',
+ iconComponent: UploadFileIcon,
+ value: 'upload',
+ text: intl.formatMessage(messages.upload),
+ },
+ {
+ icon: 'paint-brush',
+ iconComponent: BrushIcon,
+ value: 'doodle',
+ text: intl.formatMessage(messages.doodle),
+ },
+ ];
+
return (
-
+