diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml
index 96bd0bb4e5..4e6179bc77 100644
--- a/.github/workflows/chromatic.yml
+++ b/.github/workflows/chromatic.yml
@@ -20,6 +20,7 @@ jobs:
chromatic:
name: Run Chromatic
runs-on: ubuntu-latest
+ if: github.repository == 'mastodon/mastodon'
steps:
- name: Checkout code
uses: actions/checkout@v4
diff --git a/app/javascript/flavours/glitch/features/account/components/account_note.jsx b/app/javascript/flavours/glitch/features/account/components/account_note.jsx
deleted file mode 100644
index 1317df4c44..0000000000
--- a/app/javascript/flavours/glitch/features/account/components/account_note.jsx
+++ /dev/null
@@ -1,181 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-
-import { is } from 'immutable';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-import Textarea from 'react-textarea-autosize';
-
-import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
-
-const messages = defineMessages({
- placeholder: { id: 'account_note.placeholder', defaultMessage: 'Click to add a note' },
-});
-
-class InlineAlert extends PureComponent {
-
- static propTypes = {
- show: PropTypes.bool,
- };
-
- state = {
- mountMessage: false,
- };
-
- static TRANSITION_DELAY = 200;
-
- UNSAFE_componentWillReceiveProps (nextProps) {
- if (!this.props.show && nextProps.show) {
- this.setState({ mountMessage: true });
- } else if (this.props.show && !nextProps.show) {
- setTimeout(() => this.setState({ mountMessage: false }), InlineAlert.TRANSITION_DELAY);
- }
- }
-
- render () {
- const { show } = this.props;
- const { mountMessage } = this.state;
-
- return (
-
- {mountMessage && }
-
- );
- }
-
-}
-
-class AccountNote extends ImmutablePureComponent {
-
- static propTypes = {
- accountId: PropTypes.string.isRequired,
- value: PropTypes.string,
- onSave: PropTypes.func.isRequired,
- intl: PropTypes.object.isRequired,
- };
-
- state = {
- value: null,
- saving: false,
- saved: false,
- };
-
- UNSAFE_componentWillMount () {
- this._reset();
- }
-
- UNSAFE_componentWillReceiveProps (nextProps) {
- const accountWillChange = !is(this.props.accountId, nextProps.accountId);
- const newState = {};
-
- if (accountWillChange && this._isDirty()) {
- this._save(false);
- }
-
- if (accountWillChange || nextProps.value === this.state.value) {
- newState.saving = false;
- }
-
- if (this.props.value !== nextProps.value) {
- newState.value = nextProps.value;
- }
-
- this.setState(newState);
- }
-
- componentWillUnmount () {
- if (this._isDirty()) {
- this._save(false);
- }
- }
-
- setTextareaRef = c => {
- this.textarea = c;
- };
-
- handleChange = e => {
- this.setState({ value: e.target.value, saving: false });
- };
-
- handleKeyDown = e => {
- if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
- e.preventDefault();
-
- if (this.textarea) {
- this.textarea.blur();
- } else {
- this._save();
- }
- } else if (e.keyCode === 27) {
- e.preventDefault();
-
- this._reset(() => {
- if (this.textarea) {
- this.textarea.blur();
- }
- });
- }
- };
-
- handleBlur = () => {
- if (this._isDirty()) {
- this._save();
- }
- };
-
- _save (showMessage = true) {
- this.setState({ saving: true }, () => this.props.onSave(this.state.value));
-
- if (showMessage) {
- this.setState({ saved: true }, () => setTimeout(() => this.setState({ saved: false }), 2000));
- }
- }
-
- _reset (callback) {
- this.setState({ value: this.props.value }, callback);
- }
-
- _isDirty () {
- return !this.state.saving && this.props.value !== null && this.state.value !== null && this.state.value !== this.props.value;
- }
-
- render () {
- const { accountId, intl } = this.props;
- const { value, saved } = this.state;
-
- if (!accountId) {
- return null;
- }
-
- return (
-
-
-
- {this.props.value === undefined ? (
-
-
-
- ) : (
-
- )}
-
- );
- }
-
-}
-
-export default injectIntl(AccountNote);
diff --git a/app/javascript/flavours/glitch/features/account/components/account_note.tsx b/app/javascript/flavours/glitch/features/account/components/account_note.tsx
new file mode 100644
index 0000000000..36840b6bad
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/account/components/account_note.tsx
@@ -0,0 +1,131 @@
+import type { ChangeEventHandler, KeyboardEventHandler } from 'react';
+import { useState, useRef, useCallback, useId } from 'react';
+
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
+
+import Textarea from 'react-textarea-autosize';
+
+import { submitAccountNote } from '@/flavours/glitch/actions/account_notes';
+import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
+import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
+
+const messages = defineMessages({
+ placeholder: {
+ id: 'account_note.placeholder',
+ defaultMessage: 'Click to add a note',
+ },
+});
+
+const AccountNoteUI: React.FC<{
+ initialValue: string | undefined;
+ onSubmit: (newNote: string) => void;
+ wasSaved: boolean;
+}> = ({ initialValue, onSubmit, wasSaved }) => {
+ const intl = useIntl();
+ const uniqueId = useId();
+ const [value, setValue] = useState(initialValue ?? '');
+ const isLoading = initialValue === undefined;
+ const canSubmitOnBlurRef = useRef(true);
+
+ const handleChange = useCallback>(
+ (e) => {
+ setValue(e.target.value);
+ },
+ [],
+ );
+
+ const handleKeyDown = useCallback>(
+ (e) => {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+
+ setValue(initialValue ?? '');
+
+ canSubmitOnBlurRef.current = false;
+ e.currentTarget.blur();
+ } else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+
+ onSubmit(value);
+
+ canSubmitOnBlurRef.current = false;
+ e.currentTarget.blur();
+ }
+ },
+ [initialValue, onSubmit, value],
+ );
+
+ const handleBlur = useCallback(() => {
+ if (initialValue !== value && canSubmitOnBlurRef.current) {
+ onSubmit(value);
+ }
+ canSubmitOnBlurRef.current = true;
+ }, [initialValue, onSubmit, value]);
+
+ return (
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export const AccountNote: React.FC<{
+ accountId: string;
+}> = ({ accountId }) => {
+ const dispatch = useAppDispatch();
+ const initialValue = useAppSelector((state) =>
+ state.relationships.get(accountId)?.get('note'),
+ );
+ const [wasSaved, setWasSaved] = useState(false);
+
+ const handleSubmit = useCallback(
+ (note: string) => {
+ setWasSaved(true);
+ void dispatch(submitAccountNote({ accountId, note }));
+
+ setTimeout(() => {
+ setWasSaved(false);
+ }, 2000);
+ },
+ [dispatch, accountId],
+ );
+
+ return (
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/account/containers/account_note_container.js b/app/javascript/flavours/glitch/features/account/containers/account_note_container.js
deleted file mode 100644
index 448b717292..0000000000
--- a/app/javascript/flavours/glitch/features/account/containers/account_note_container.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { connect } from 'react-redux';
-
-import { submitAccountNote } from 'flavours/glitch/actions/account_notes';
-
-import AccountNote from '../components/account_note';
-
-const mapStateToProps = (state, { accountId }) => ({
- value: state.relationships.getIn([accountId, 'note']),
-});
-
-const mapDispatchToProps = (dispatch, { accountId }) => ({
-
- onSave (value) {
- dispatch(submitAccountNote({ accountId: accountId, note: value }));
- },
-
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(AccountNote);
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/account_header.tsx b/app/javascript/flavours/glitch/features/account_timeline/components/account_header.tsx
index 6baa47eb4b..188cdc0824 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/account_header.tsx
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/account_header.tsx
@@ -42,8 +42,8 @@ import { FollowButton } from 'flavours/glitch/components/follow_button';
import { FormattedDateWrapper } from 'flavours/glitch/components/formatted_date';
import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
+import { AccountNote } from 'flavours/glitch/features/account/components/account_note';
import { DomainPill } from 'flavours/glitch/features/account/components/domain_pill';
-import AccountNoteContainer from 'flavours/glitch/features/account/containers/account_note_container';
import FollowRequestNoteContainer from 'flavours/glitch/features/account/containers/follow_request_note_container';
import { useLinks } from 'flavours/glitch/hooks/useLinks';
import { useIdentity } from 'flavours/glitch/identity_context';
@@ -927,7 +927,7 @@ export const AccountHeader: React.FC<{
onClickCapture={handleLinkClick}
>
{account.id !== me && signedIn && (
-
+
)}
{account.note.length > 0 && account.note !== '' && (
diff --git a/app/javascript/flavours/glitch/features/compose/components/search.tsx b/app/javascript/flavours/glitch/features/compose/components/search.tsx
index 97ab67c6c2..cb94930ffc 100644
--- a/app/javascript/flavours/glitch/features/compose/components/search.tsx
+++ b/app/javascript/flavours/glitch/features/compose/components/search.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useState, useRef } from 'react';
+import { useCallback, useState, useRef, useEffect } from 'react';
import {
defineMessages,
@@ -72,6 +72,10 @@ export const Search: React.FC<{
const [expanded, setExpanded] = useState(false);
const [selectedOption, setSelectedOption] = useState(-1);
const [quickActions, setQuickActions] = useState([]);
+ useEffect(() => {
+ setValue(initialValue ?? '');
+ setQuickActions([]);
+ }, [initialValue]);
const searchOptions: SearchOption[] = [];
if (searchEnabled) {
diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss
index 421ffbada3..bbd3b88fbd 100644
--- a/app/javascript/flavours/glitch/styles/components.scss
+++ b/app/javascript/flavours/glitch/styles/components.scss
@@ -1948,16 +1948,18 @@ body > [data-popper-placement] {
}
.status__quote {
+ --quote-margin: 36px;
+
position: relative;
margin-block-start: 16px;
- margin-inline-start: 36px;
+ margin-inline-start: calc(var(--quote-margin) + var(--thread-margin, 0px));
border-radius: 8px;
color: var(--nested-card-text);
background: var(--nested-card-background);
border: var(--nested-card-border);
- @media screen and (min-width: $mobile-breakpoint) {
- margin-inline-start: 56px;
+ @container (width > 460px) {
+ --quote-margin: 56px;
}
}
@@ -2017,7 +2019,7 @@ body > [data-popper-placement] {
transform: translateY(-50%);
}
- @media screen and (min-width: $mobile-breakpoint) {
+ @container (width > 460px) {
inset-inline-start: -50px;
}
}
@@ -2943,6 +2945,7 @@ a.account__display-name {
display: flex;
flex-direction: column;
contain: inline-size layout paint style;
+ container: column / inline-size;
@media screen and (min-width: $no-gap-breakpoint) {
max-width: 600px;
diff --git a/app/javascript/mastodon/features/compose/components/search.tsx b/app/javascript/mastodon/features/compose/components/search.tsx
index 2186ff36ab..30a7a84db6 100644
--- a/app/javascript/mastodon/features/compose/components/search.tsx
+++ b/app/javascript/mastodon/features/compose/components/search.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useState, useRef } from 'react';
+import { useCallback, useState, useRef, useEffect } from 'react';
import {
defineMessages,
@@ -72,6 +72,10 @@ export const Search: React.FC<{
const [expanded, setExpanded] = useState(false);
const [selectedOption, setSelectedOption] = useState(-1);
const [quickActions, setQuickActions] = useState([]);
+ useEffect(() => {
+ setValue(initialValue ?? '');
+ setQuickActions([]);
+ }, [initialValue]);
const searchOptions: SearchOption[] = [];
if (searchEnabled) {
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 0122b81b8b..d31b6f0e3d 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1882,16 +1882,18 @@ body > [data-popper-placement] {
}
.status__quote {
+ --quote-margin: 36px;
+
position: relative;
margin-block-start: 16px;
- margin-inline-start: 36px;
+ margin-inline-start: calc(var(--quote-margin) + var(--thread-margin, 0px));
border-radius: 8px;
color: var(--nested-card-text);
background: var(--nested-card-background);
border: var(--nested-card-border);
- @media screen and (min-width: $mobile-breakpoint) {
- margin-inline-start: 56px;
+ @container (width > 460px) {
+ --quote-margin: 56px;
}
}
@@ -1951,7 +1953,7 @@ body > [data-popper-placement] {
transform: translateY(-50%);
}
- @media screen and (min-width: $mobile-breakpoint) {
+ @container (width > 460px) {
inset-inline-start: -50px;
}
}
@@ -2878,6 +2880,7 @@ a.account__display-name {
display: flex;
flex-direction: column;
contain: inline-size layout paint style;
+ container: column / inline-size;
@media screen and (min-width: $no-gap-breakpoint) {
max-width: 600px;
diff --git a/app/lib/admin/system_check/sidekiq_process_check.rb b/app/lib/admin/system_check/sidekiq_process_check.rb
index d577b3bf3c..f55cb7a2f0 100644
--- a/app/lib/admin/system_check/sidekiq_process_check.rb
+++ b/app/lib/admin/system_check/sidekiq_process_check.rb
@@ -8,6 +8,7 @@ class Admin::SystemCheck::SidekiqProcessCheck < Admin::SystemCheck::BaseCheck
pull
scheduler
ingress
+ fasp
).freeze
def skip?
diff --git a/app/models/concerns/status/safe_reblog_insert.rb b/app/models/concerns/status/safe_reblog_insert.rb
index 48d585ea18..94bed5b39a 100644
--- a/app/models/concerns/status/safe_reblog_insert.rb
+++ b/app/models/concerns/status/safe_reblog_insert.rb
@@ -16,7 +16,7 @@ module Status::SafeReblogInsert
# The code is kept similar to ActiveRecord::Persistence code and calls it
# directly when we are not handling a reblog.
#
- # https://github.com/rails/rails/blob/v7.2.1.1/activerecord/lib/active_record/persistence.rb#L238-L263
+ # https://github.com/rails/rails/blob/v8.0.2/activerecord/lib/active_record/persistence.rb#L238-L261
def _insert_record(connection, values, returning)
return super unless values.is_a?(Hash) && values['reblog_of_id']&.value.present?
@@ -36,15 +36,13 @@ module Status::SafeReblogInsert
# Instead, we use a custom builder when a reblog is happening:
im = _compile_reblog_insert(values)
- with_connection do |_c|
- connection.insert(
- im, "#{self} Create", primary_key || false, primary_key_value,
- returning: returning
- ).tap do |result|
- # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible.
- # For our purposes, it's equivalent to a foreign key constraint violation
- raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id'].value}) is not present in table \"statuses\"" if result.nil?
- end
+ connection.insert(
+ im, "#{self} Create", primary_key || false, primary_key_value,
+ returning: returning
+ ).tap do |result|
+ # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible.
+ # For our purposes, it's equivalent to a foreign key constraint violation
+ raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id'].value}) is not present in table \"statuses\"" if result.nil?
end
end
diff --git a/spec/lib/admin/system_check/sidekiq_process_check_spec.rb b/spec/lib/admin/system_check/sidekiq_process_check_spec.rb
index 992fd7aee0..c32f4d736c 100644
--- a/spec/lib/admin/system_check/sidekiq_process_check_spec.rb
+++ b/spec/lib/admin/system_check/sidekiq_process_check_spec.rb
@@ -35,11 +35,11 @@ RSpec.describe Admin::SystemCheck::SidekiqProcessCheck do
describe 'message' do
it 'sends values to message instance' do
- allow(Admin::SystemCheck::Message).to receive(:new).with(:sidekiq_process_check, 'default, push, mailers, pull, scheduler, ingress')
+ allow(Admin::SystemCheck::Message).to receive(:new).with(:sidekiq_process_check, 'default, push, mailers, pull, scheduler, ingress, fasp')
check.message
- expect(Admin::SystemCheck::Message).to have_received(:new).with(:sidekiq_process_check, 'default, push, mailers, pull, scheduler, ingress')
+ expect(Admin::SystemCheck::Message).to have_received(:new).with(:sidekiq_process_check, 'default, push, mailers, pull, scheduler, ingress, fasp')
end
end
end