diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19be8ea68e..97a7234382 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,26 @@
All notable changes to this project will be documented in this file.
+## [4.4.2] - 2025-07-23
+
+### Security
+
+- Update dependencies
+
+### Fixed
+
+- Fix menu not clickable in Firefox (#35390 and #35414 by @diondiondion)
+- Add `lang` attribute to current composer language in alt text modal (#35412 by @diondiondion)
+- Fix quote posts styling on notifications page (#35411 by @diondiondion)
+- Improve a11y of custom select menus in notifications settings (#35403 by @diondiondion)
+- Fix selected item in poll select menus is unreadable in Firefox (#35402 by @diondiondion)
+- Update age limit wording (#35387 by @diondiondion)
+- Fix support for quote verification in implicit status updates (#35384 by @ClearlyClaire)
+- Improve `Dropdown` component accessibility (#35373 by @diondiondion)
+- Fix processing some incoming quotes failing because of missing JSON-LD context (#35354 and #35380 by @ClearlyClaire)
+- Make bio hashtags open the local page instead of the remote instance (#35349 by @ChaosExAnima)
+- Fix styling of external log-in button (#35320 by @ClearlyClaire)
+
## [4.4.1] - 2025-07-09
### Fixed
diff --git a/Gemfile.lock b/Gemfile.lock
index 299507cac9..de7b7cfd0c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -458,7 +458,7 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
- nokogiri (1.18.8)
+ nokogiri (1.18.9)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.16.11)
@@ -869,7 +869,7 @@ GEM
terrapin (1.1.0)
climate_control
test-prof (1.4.4)
- thor (1.3.2)
+ thor (1.4.0)
tilt (2.6.0)
timeout (0.4.3)
tpm-key_attestation (0.14.1)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 90f8fd000c..cb349f4d51 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -66,7 +66,7 @@ module ApplicationHelper
def provider_sign_in_link(provider)
label = Devise.omniauth_configs[provider]&.strategy&.display_name.presence || I18n.t("auth.providers.#{provider}", default: provider.to_s.chomp('_oauth2').capitalize)
- link_to label, omniauth_authorize_path(:user, provider), class: "button button-#{provider}", method: :post
+ link_to label, omniauth_authorize_path(:user, provider), class: "btn button-#{provider}", method: :post
end
def locale_direction
diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb
index 22d1964cae..9972b507cd 100644
--- a/app/helpers/context_helper.rb
+++ b/app/helpers/context_helper.rb
@@ -27,6 +27,12 @@ module ContextHelper
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
+ quotes: {
+ 'quote' => 'https://w3id.org/fep/044f#quote',
+ 'quoteUri' => 'http://fedibird.com/ns#quoteUri',
+ '_misskey_quote' => 'https://misskey-hub.net/ns#_misskey_quote',
+ 'quoteAuthorization' => { '@id' => 'https://w3id.org/fep/044f#quoteAuthorization', '@type' => '@id' },
+ },
interaction_policies: {
'gts' => 'https://gotosocial.org/ns#',
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },
diff --git a/app/javascript/mastodon/components/account_bio.tsx b/app/javascript/mastodon/components/account_bio.tsx
index 301ffcbb24..e0127f2092 100644
--- a/app/javascript/mastodon/components/account_bio.tsx
+++ b/app/javascript/mastodon/components/account_bio.tsx
@@ -1,12 +1,30 @@
+import { useCallback } from 'react';
+
import { useLinks } from 'mastodon/hooks/useLinks';
-export const AccountBio: React.FC<{
+interface AccountBioProps {
note: string;
className: string;
-}> = ({ note, className }) => {
- const handleClick = useLinks();
+ dropdownAccountId?: string;
+}
- if (note.length === 0 || note === '
') {
+export const AccountBio: React.FC = ({
+ note,
+ className,
+ dropdownAccountId,
+}) => {
+ const handleClick = useLinks(!!dropdownAccountId);
+ const handleNodeChange = useCallback(
+ (node: HTMLDivElement | null) => {
+ if (!dropdownAccountId || !node || node.childNodes.length === 0) {
+ return;
+ }
+ addDropdownToHashtags(node, dropdownAccountId);
+ },
+ [dropdownAccountId],
+ );
+
+ if (note.length === 0) {
return null;
}
@@ -15,6 +33,28 @@ export const AccountBio: React.FC<{
className={`${className} translate`}
dangerouslySetInnerHTML={{ __html: note }}
onClickCapture={handleClick}
+ ref={handleNodeChange}
/>
);
};
+
+function addDropdownToHashtags(node: HTMLElement | null, accountId: string) {
+ if (!node) {
+ return;
+ }
+ for (const childNode of node.childNodes) {
+ if (!(childNode instanceof HTMLElement)) {
+ continue;
+ }
+ if (
+ childNode instanceof HTMLAnchorElement &&
+ (childNode.classList.contains('hashtag') ||
+ childNode.innerText.startsWith('#')) &&
+ !childNode.dataset.menuHashtag
+ ) {
+ childNode.dataset.menuHashtag = accountId;
+ } else if (childNode.childNodes.length > 0) {
+ addDropdownToHashtags(childNode, accountId);
+ }
+ }
+}
diff --git a/app/javascript/mastodon/components/dropdown_menu.tsx b/app/javascript/mastodon/components/dropdown_menu.tsx
index 23d77f0dda..d9c87e93a7 100644
--- a/app/javascript/mastodon/components/dropdown_menu.tsx
+++ b/app/javascript/mastodon/components/dropdown_menu.tsx
@@ -5,6 +5,7 @@ import {
useCallback,
cloneElement,
Children,
+ useId,
} from 'react';
import classNames from 'classnames';
@@ -16,6 +17,7 @@ import Overlay from 'react-overlays/Overlay';
import type {
OffsetValue,
UsePopperOptions,
+ Placement,
} from 'react-overlays/esm/usePopper';
import { fetchRelationships } from 'mastodon/actions/accounts';
@@ -295,6 +297,11 @@ interface DropdownProps- {
title?: string;
disabled?: boolean;
scrollable?: boolean;
+ placement?: Placement;
+ /**
+ * Prevent the `ScrollableList` with this scrollKey
+ * from being scrolled while the dropdown is open
+ */
scrollKey?: string;
status?: ImmutableMap;
forceDropdown?: boolean;
@@ -316,6 +323,7 @@ export const Dropdown =
- ({
title = 'Menu',
disabled,
scrollable,
+ placement = 'bottom',
status,
forceDropdown = false,
renderItem,
@@ -331,16 +339,15 @@ export const Dropdown =
- ({
);
const [currentId] = useState(id++);
const open = currentId === openDropdownId;
- const activeElement = useRef(null);
- const targetRef = useRef(null);
+ const buttonRef = useRef(null);
+ const menuId = useId();
const prefetchAccountId = status
? status.getIn(['account', 'id'])
: undefined;
const handleClose = useCallback(() => {
- if (activeElement.current) {
- activeElement.current.focus({ preventScroll: true });
- activeElement.current = null;
+ if (buttonRef.current) {
+ buttonRef.current.focus({ preventScroll: true });
}
dispatch(
@@ -375,7 +382,7 @@ export const Dropdown =
- ({
[handleClose, onItemClick, items],
);
- const handleClick = useCallback(
+ const toggleDropdown = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const { type } = e;
@@ -423,38 +430,6 @@ export const Dropdown =
- ({
],
);
- const handleMouseDown = useCallback(() => {
- if (!open && document.activeElement instanceof HTMLElement) {
- activeElement.current = document.activeElement;
- }
- }, [open]);
-
- const handleButtonKeyDown = useCallback(
- (e: React.KeyboardEvent) => {
- switch (e.key) {
- case ' ':
- case 'Enter':
- handleMouseDown();
- break;
- }
- },
- [handleMouseDown],
- );
-
- const handleKeyPress = useCallback(
- (e: React.KeyboardEvent) => {
- switch (e.key) {
- case ' ':
- case 'Enter':
- handleClick(e);
- e.stopPropagation();
- e.preventDefault();
- break;
- }
- },
- [handleClick],
- );
-
useEffect(() => {
return () => {
if (currentId === openDropdownId) {
@@ -465,14 +440,16 @@ export const Dropdown =
- ({
let button: React.ReactElement;
+ const buttonProps = {
+ disabled,
+ onClick: toggleDropdown,
+ 'aria-expanded': open,
+ 'aria-controls': menuId,
+ ref: buttonRef,
+ };
+
if (children) {
- button = cloneElement(Children.only(children), {
- onClick: handleClick,
- onMouseDown: handleMouseDown,
- onKeyDown: handleButtonKeyDown,
- onKeyPress: handleKeyPress,
- ref: targetRef,
- });
+ button = cloneElement(Children.only(children), buttonProps);
} else if (icon && iconComponent) {
button = (
({
iconComponent={iconComponent}
title={title}
active={open}
- disabled={disabled}
- onClick={handleClick}
- onMouseDown={handleMouseDown}
- onKeyDown={handleButtonKeyDown}
- onKeyPress={handleKeyPress}
- ref={targetRef}
+ {...buttonProps}
/>
);
} else {
@@ -499,13 +471,13 @@ export const Dropdown =
- ({
{({ props, arrowProps, placement }) => (
-
+