Compare commits

...

4 Commits

Author SHA1 Message Date
Matt Jankowski
22d044adc0
Merge 80350a2b48 into 3b52dca405 2025-07-11 17:06:11 +00:00
Claire
3b52dca405
Fix quote attributes missing from Mastodon's context (#35354)
Some checks failed
Check i18n / check-i18n (push) Has been cancelled
Chromatic / Run Chromatic (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (ruby) (push) Has been cancelled
Check formatting / lint (push) Has been cancelled
JavaScript Linting / lint (push) Has been cancelled
Ruby Linting / lint (push) Has been cancelled
JavaScript Testing / test (push) Has been cancelled
Historical data migration test / test (14-alpine) (push) Has been cancelled
Historical data migration test / test (15-alpine) (push) Has been cancelled
Historical data migration test / test (16-alpine) (push) Has been cancelled
Historical data migration test / test (17-alpine) (push) Has been cancelled
Ruby Testing / build (production) (push) Has been cancelled
Ruby Testing / build (test) (push) Has been cancelled
Ruby Testing / test (.ruby-version) (push) Has been cancelled
Ruby Testing / test (3.2) (push) Has been cancelled
Ruby Testing / test (3.3) (push) Has been cancelled
Ruby Testing / ImageMagick tests (.ruby-version) (push) Has been cancelled
Ruby Testing / ImageMagick tests (3.2) (push) Has been cancelled
Ruby Testing / ImageMagick tests (3.3) (push) Has been cancelled
Ruby Testing / End to End testing (.ruby-version) (push) Has been cancelled
Ruby Testing / End to End testing (3.2) (push) Has been cancelled
Ruby Testing / End to End testing (3.3) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Has been cancelled
2025-07-11 16:35:06 +00:00
Echo
853a0c466e
Make bio hashtags open the local page instead of the remote instance (#35349) 2025-07-11 15:18:34 +00:00
Matt Jankowski
80350a2b48 Move x.single_user_mode config into mastodon.x area 2025-07-09 07:55:28 -04:00
15 changed files with 82 additions and 37 deletions

View File

@ -148,7 +148,7 @@ class ApplicationController < ActionController::Base
end end
def single_user_mode? def single_user_mode?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.without_internal.exists? @single_user_mode ||= Rails.configuration.x.mastodon.single_user_mode && Account.without_internal.exists?
end end
def use_seamless_external_login? def use_seamless_external_login?

View File

@ -26,6 +26,12 @@ module ContextHelper
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } }, attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' }, 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' => 'https://w3id.org/fep/044f#quoteAuthorization',
},
interaction_policies: { interaction_policies: {
'gts' => 'https://gotosocial.org/ns#', 'gts' => 'https://gotosocial.org/ns#',
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' }, 'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },

View File

@ -4,7 +4,7 @@ module RegistrationHelper
extend ActiveSupport::Concern extend ActiveSupport::Concern
def allowed_registration?(remote_ip, invite) def allowed_registration?(remote_ip, invite)
!Rails.configuration.x.single_user_mode && !omniauth_only? && (registrations_open? || invite&.valid_for_use?) && !ip_blocked?(remote_ip) !Rails.configuration.x.mastodon.single_user_mode && !omniauth_only? && (registrations_open? || invite&.valid_for_use?) && !ip_blocked?(remote_ip)
end end
def registrations_open? def registrations_open?

View File

@ -1,12 +1,30 @@
import { useCallback } from 'react';
import { useLinks } from 'mastodon/hooks/useLinks'; import { useLinks } from 'mastodon/hooks/useLinks';
export const AccountBio: React.FC<{ interface AccountBioProps {
note: string; note: string;
className: string; className: string;
}> = ({ note, className }) => { dropdownAccountId?: string;
const handleClick = useLinks(); }
if (note.length === 0 || note === '<p></p>') { export const AccountBio: React.FC<AccountBioProps> = ({
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; return null;
} }
@ -15,6 +33,28 @@ export const AccountBio: React.FC<{
className={`${className} translate`} className={`${className} translate`}
dangerouslySetInnerHTML={{ __html: note }} dangerouslySetInnerHTML={{ __html: note }}
onClickCapture={handleClick} 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);
}
}
}

View File

@ -6,6 +6,7 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { AccountBio } from '@/mastodon/components/account_bio';
import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
@ -773,7 +774,6 @@ export const AccountHeader: React.FC<{
); );
} }
const content = { __html: account.note_emojified };
const displayNameHtml = { __html: account.display_name_html }; const displayNameHtml = { __html: account.display_name_html };
const fields = account.fields; const fields = account.fields;
const isLocal = !account.acct.includes('@'); const isLocal = !account.acct.includes('@');
@ -897,12 +897,11 @@ export const AccountHeader: React.FC<{
<AccountNote accountId={accountId} /> <AccountNote accountId={accountId} />
)} )}
{account.note.length > 0 && account.note !== '<p></p>' && ( <AccountBio
<div note={account.note_emojified}
className='account__header__content translate' dropdownAccountId={accountId}
dangerouslySetInnerHTML={content} className='account__header__content'
/> />
)}
<div className='account__header__fields'> <div className='account__header__fields'>
<dl> <dl>

View File

@ -8,13 +8,14 @@ import { openURL } from 'mastodon/actions/search';
import { useAppDispatch } from 'mastodon/store'; import { useAppDispatch } from 'mastodon/store';
const isMentionClick = (element: HTMLAnchorElement) => const isMentionClick = (element: HTMLAnchorElement) =>
element.classList.contains('mention'); element.classList.contains('mention') &&
!element.classList.contains('hashtag');
const isHashtagClick = (element: HTMLAnchorElement) => const isHashtagClick = (element: HTMLAnchorElement) =>
element.textContent?.[0] === '#' || element.textContent?.[0] === '#' ||
element.previousSibling?.textContent?.endsWith('#'); element.previousSibling?.textContent?.endsWith('#');
export const useLinks = () => { export const useLinks = (skipHashtags?: boolean) => {
const history = useHistory(); const history = useHistory();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -61,12 +62,12 @@ export const useLinks = () => {
if (isMentionClick(target)) { if (isMentionClick(target)) {
e.preventDefault(); e.preventDefault();
void handleMentionClick(target); void handleMentionClick(target);
} else if (isHashtagClick(target)) { } else if (isHashtagClick(target) && !skipHashtags) {
e.preventDefault(); e.preventDefault();
handleHashtagClick(target); handleHashtagClick(target);
} }
}, },
[handleMentionClick, handleHashtagClick], [skipHashtags, handleMentionClick, handleHashtagClick],
); );
return handleClick; return handleClick;

View File

@ -126,6 +126,9 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
? accountJSON.username ? accountJSON.username
: accountJSON.display_name; : accountJSON.display_name;
const accountNote =
accountJSON.note && accountJSON.note !== '<p></p>' ? accountJSON.note : '';
return AccountFactory({ return AccountFactory({
...accountJSON, ...accountJSON,
moved: moved?.id, moved: moved?.id,
@ -142,8 +145,8 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
escapeTextContentForBrowser(displayName), escapeTextContentForBrowser(displayName),
emojiMap, emojiMap,
), ),
note_emojified: emojify(accountJSON.note, emojiMap), note_emojified: emojify(accountNote, emojiMap),
note_plain: unescapeHTML(accountJSON.note), note_plain: unescapeHTML(accountNote),
url: url:
accountJSON.url.startsWith('http://') || accountJSON.url.startsWith('http://') ||
accountJSON.url.startsWith('https://') accountJSON.url.startsWith('https://')

View File

@ -40,7 +40,7 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:disabled_account_id] = object.disabled_account.id.to_s if object.disabled_account store[:disabled_account_id] = object.disabled_account.id.to_s if object.disabled_account
store[:moved_to_account_id] = object.moved_to_account.id.to_s if object.moved_to_account store[:moved_to_account_id] = object.moved_to_account.id.to_s if object.moved_to_account
store[:owner] = object.owner&.id&.to_s if Rails.configuration.x.single_user_mode store[:owner] = object.owner&.id&.to_s if Rails.configuration.x.mastodon.single_user_mode
store store
end end
@ -101,10 +101,10 @@ class InitialStateSerializer < ActiveModel::Serializer
locale: I18n.locale, locale: I18n.locale,
mascot: instance_presenter.mascot&.file&.url, mascot: instance_presenter.mascot&.file&.url,
profile_directory: Setting.profile_directory, profile_directory: Setting.profile_directory,
registrations_open: Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode, registrations_open: Setting.registrations_mode != 'none' && !Rails.configuration.x.mastodon.single_user_mode,
repository: Mastodon::Version.repository, repository: Mastodon::Version.repository,
search_enabled: Chewy.enabled?, search_enabled: Chewy.enabled?,
single_user_mode: Rails.configuration.x.single_user_mode, single_user_mode: Rails.configuration.x.mastodon.single_user_mode,
source_url: instance_presenter.source_url, source_url: instance_presenter.source_url,
sso_redirect: sso_redirect, sso_redirect: sso_redirect,
status_page_url: Setting.status_page_url, status_page_url: Setting.status_page_url,

View File

@ -34,7 +34,7 @@ class NodeInfo::Serializer < ActiveModel::Serializer
end end
def open_registrations def open_registrations
Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode Setting.registrations_mode != 'none' && !Rails.configuration.x.mastodon.single_user_mode
end end
def metadata def metadata

View File

@ -122,7 +122,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
private private
def registrations_enabled? def registrations_enabled?
Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode Setting.registrations_mode != 'none' && !Rails.configuration.x.mastodon.single_user_mode
end end
def registrations_message def registrations_message

View File

@ -79,7 +79,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
end end
def registrations def registrations
Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode Setting.registrations_mode != 'none' && !Rails.configuration.x.mastodon.single_user_mode
end end
def approval_required def approval_required

View File

@ -1,5 +0,0 @@
# frozen_string_literal: true
Rails.application.configure do
config.x.single_user_mode = ENV['SINGLE_USER_MODE'] == 'true'
end

View File

@ -3,6 +3,7 @@ shared:
experimental_features: <%= ENV.fetch('EXPERIMENTAL_FEATURES', nil) %> experimental_features: <%= ENV.fetch('EXPERIMENTAL_FEATURES', nil) %>
limited_federation_mode: <%= (ENV.fetch('LIMITED_FEDERATION_MODE', nil) || ENV.fetch('WHITELIST_MODE', nil)) == 'true' %> limited_federation_mode: <%= (ENV.fetch('LIMITED_FEDERATION_MODE', nil) || ENV.fetch('WHITELIST_MODE', nil)) == 'true' %>
self_destruct_value: <%= ENV.fetch('SELF_DESTRUCT', nil)&.to_json %> self_destruct_value: <%= ENV.fetch('SELF_DESTRUCT', nil)&.to_json %>
single_user_mode: <%= ENV.fetch('SINGLE_USER_MODE', nil) == 'true' %>
software_update_url: <%= ENV.fetch('UPDATE_CHECK_URL', 'https://api.joinmastodon.org/update-check')&.to_json %> software_update_url: <%= ENV.fetch('UPDATE_CHECK_URL', 'https://api.joinmastodon.org/update-check')&.to_json %>
source: source:
base_url: <%= ENV.fetch('SOURCE_BASE_URL', nil)&.to_json %> base_url: <%= ENV.fetch('SOURCE_BASE_URL', nil)&.to_json %>

View File

@ -68,18 +68,18 @@ RSpec.describe ApplicationController do
describe 'helper_method :single_user_mode?' do describe 'helper_method :single_user_mode?' do
it 'returns false if it is in single_user_mode but there is no account' do it 'returns false if it is in single_user_mode but there is no account' do
allow(Rails.configuration.x).to receive(:single_user_mode).and_return(true) allow(Rails.configuration.x.mastodon).to receive(:single_user_mode).and_return(true)
expect(controller.view_context.single_user_mode?).to be false expect(controller.view_context.single_user_mode?).to be false
end end
it 'returns false if there is an account but it is not in single_user_mode' do it 'returns false if there is an account but it is not in single_user_mode' do
allow(Rails.configuration.x).to receive(:single_user_mode).and_return(false) allow(Rails.configuration.x.mastodon).to receive(:single_user_mode).and_return(false)
Fabricate(:account) Fabricate(:account)
expect(controller.view_context.single_user_mode?).to be false expect(controller.view_context.single_user_mode?).to be false
end end
it 'returns true if it is in single_user_mode and there is an account' do it 'returns true if it is in single_user_mode and there is an account' do
allow(Rails.configuration.x).to receive(:single_user_mode).and_return(true) allow(Rails.configuration.x.mastodon).to receive(:single_user_mode).and_return(true)
Fabricate(:account) Fabricate(:account)
expect(controller.view_context.single_user_mode?).to be true expect(controller.view_context.single_user_mode?).to be true
end end

View File

@ -9,7 +9,7 @@ RSpec.describe Auth::RegistrationsController do
context 'when in single user mode and open for registration' do context 'when in single user mode and open for registration' do
before do before do
Setting.registrations_mode = 'open' Setting.registrations_mode = 'open'
allow(Rails.configuration.x).to receive(:single_user_mode).and_return(true) allow(Rails.configuration.x.mastodon).to receive(:single_user_mode).and_return(true)
end end
it 'redirects to root' do it 'redirects to root' do
@ -17,21 +17,21 @@ RSpec.describe Auth::RegistrationsController do
get path get path
expect(response).to redirect_to '/' expect(response).to redirect_to '/'
expect(Rails.configuration.x).to have_received(:single_user_mode) expect(Rails.configuration.x.mastodon).to have_received(:single_user_mode)
end end
end end
context 'when registrations closed and not in single user mode' do context 'when registrations closed and not in single user mode' do
before do before do
Setting.registrations_mode = 'none' Setting.registrations_mode = 'none'
allow(Rails.configuration.x).to receive(:single_user_mode).and_return(false) allow(Rails.configuration.x.mastodon).to receive(:single_user_mode).and_return(false)
end end
it 'redirects to root' do it 'redirects to root' do
get path get path
expect(response).to redirect_to '/' expect(response).to redirect_to '/'
expect(Rails.configuration.x).to have_received(:single_user_mode) expect(Rails.configuration.x.mastodon).to have_received(:single_user_mode)
end end
end end
end end