Merge remote-tracking branch 'parent/main' into kbtopic-remove-quote

This commit is contained in:
KMY 2025-05-29 12:52:49 +09:00
commit c538c23ef7
141 changed files with 1552 additions and 779 deletions

View file

@ -0,0 +1,44 @@
import type { FC } from 'react';
import type { IntlShape } from 'react-intl';
import { defineMessages } from 'react-intl';
import { Icon } from '@/mastodon/components/icon';
import DisabledIcon from '@/material-icons/400-24px/close-fill.svg?react';
import EnabledIcon from '@/material-icons/400-24px/done-fill.svg?react';
const messages = defineMessages({
enabled: { id: 'about.enabled', defaultMessage: 'Enabled' },
disabled: { id: 'about.disabled', defaultMessage: 'Disabled' },
});
interface CapabilityIconProps {
intl: IntlShape;
state: boolean;
}
export const CapabilityIcon: FC<CapabilityIconProps> = ({ intl, state }) => {
if (state) {
return (
<span className='capability-icon enabled'>
<Icon
id='check'
icon={EnabledIcon}
title={intl.formatMessage(messages.enabled)}
/>
{intl.formatMessage(messages.enabled)}
</span>
);
} else {
return (
<span className='capability-icon disabled'>
<Icon
id='times'
icon={DisabledIcon}
title={intl.formatMessage(messages.disabled)}
/>
{intl.formatMessage(messages.disabled)}
</span>
);
}
};

View file

@ -0,0 +1,156 @@
import { useCallback, useState } from 'react';
import type { ChangeEventHandler, FC } from 'react';
import type { IntlShape } from 'react-intl';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { createSelector } from '@reduxjs/toolkit';
import type { List as ImmutableList } from 'immutable';
import type { SelectItem } from '@/mastodon/components/dropdown_selector';
import type { RootState } from '@/mastodon/store';
import { useAppSelector } from '@/mastodon/store';
import { Section } from './section';
const messages = defineMessages({
rules: { id: 'about.rules', defaultMessage: 'Server rules' },
defaultLocale: { id: 'about.default_locale', defaultMessage: 'Default' },
});
interface RulesSectionProps {
isLoading?: boolean;
}
interface BaseRule {
text: string;
hint: string;
}
interface Rule extends BaseRule {
id: string;
translations: Record<string, BaseRule>;
}
export const RulesSection: FC<RulesSectionProps> = ({ isLoading = false }) => {
const intl = useIntl();
const [locale, setLocale] = useState(intl.locale);
const rules = useAppSelector((state) => rulesSelector(state, locale));
const localeOptions = useAppSelector((state) =>
localeOptionsSelector(state, intl),
);
const handleLocaleChange: ChangeEventHandler<HTMLSelectElement> = useCallback(
(e) => {
setLocale(e.currentTarget.value);
},
[],
);
if (isLoading) {
return <Section title={intl.formatMessage(messages.rules)} />;
}
if (rules.length === 0) {
return (
<Section title={intl.formatMessage(messages.rules)}>
<p>
<FormattedMessage
id='about.not_available'
defaultMessage='This information has not been made available on this server.'
/>
</p>
</Section>
);
}
return (
<Section title={intl.formatMessage(messages.rules)}>
<ol className='rules-list'>
{rules.map((rule) => (
<li key={rule.id}>
<div className='rules-list__text'>{rule.text}</div>
{!!rule.hint && <div className='rules-list__hint'>{rule.hint}</div>}
</li>
))}
</ol>
<div className='rules-languages'>
<label htmlFor='language-select'>
<FormattedMessage
id='about.language_label'
defaultMessage='Language'
/>
</label>
<select onChange={handleLocaleChange} id='language-select'>
{localeOptions.map((option) => (
<option
key={option.value}
value={option.value}
selected={option.value === locale}
>
{option.text}
</option>
))}
</select>
</div>
</Section>
);
};
const selectRules = (state: RootState) => {
const rules = state.server.getIn([
'server',
'rules',
]) as ImmutableList<Rule> | null;
if (!rules) {
return [];
}
return rules.toJS() as Rule[];
};
const rulesSelector = createSelector(
[selectRules, (_state, locale: string) => locale],
(rules, locale): Rule[] => {
return rules.map((rule) => {
const translations = rule.translations;
if (translations[locale]) {
rule.text = translations[locale].text;
rule.hint = translations[locale].hint;
}
const partialLocale = locale.split('-')[0];
if (partialLocale && translations[partialLocale]) {
rule.text = translations[partialLocale].text;
rule.hint = translations[partialLocale].hint;
}
return rule;
});
},
);
const localeOptionsSelector = createSelector(
[selectRules, (_state, intl: IntlShape) => intl],
(rules, intl): SelectItem[] => {
const langs: Record<string, SelectItem> = {
default: {
value: 'default',
text: intl.formatMessage(messages.defaultLocale),
},
};
// Use the default locale as a target to translate language names.
const intlLocale = new Intl.DisplayNames(intl.locale, {
type: 'language',
});
for (const { translations } of rules) {
for (const locale in translations) {
if (langs[locale]) {
continue; // Skip if already added
}
langs[locale] = {
value: locale,
text: intlLocale.of(locale) ?? locale,
};
}
}
return Object.values(langs);
},
);

View file

@ -0,0 +1,45 @@
import type { FC, MouseEventHandler } from 'react';
import { useCallback, useState } from 'react';
import classNames from 'classnames';
import { Icon } from '@/mastodon/components/icon';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import ExpandMoreIcon from '@/material-icons/400-24px/expand_more.svg?react';
interface SectionProps {
title: string;
children?: React.ReactNode;
open?: boolean;
onOpen?: () => void;
}
export const Section: FC<SectionProps> = ({
title,
children,
open = false,
onOpen,
}) => {
const [collapsed, setCollapsed] = useState(!open);
const handleClick: MouseEventHandler = useCallback(() => {
setCollapsed((prev) => !prev);
onOpen?.();
}, [onOpen]);
return (
<div className={classNames('about__section', { active: !collapsed })}>
<button
className='about__section__title'
tabIndex={0}
onClick={handleClick}
>
<Icon
id={collapsed ? 'chevron-right' : 'chevron-down'}
icon={collapsed ? ChevronRightIcon : ExpandMoreIcon}
/>{' '}
{title}
</button>
{!collapsed && <div className='about__section__body'>{children}</div>}
</div>
);
};

View file

@ -3,28 +3,24 @@ import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import DisabledIcon from '@/material-icons/400-24px/close-fill.svg?react';
import EnabledIcon from '@/material-icons/400-24px/done-fill.svg?react';
import ExpandMoreIcon from '@/material-icons/400-24px/expand_more.svg?react';
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server';
import { Account } from 'mastodon/components/account';
import Column from 'mastodon/components/column';
import { Icon } from 'mastodon/components/icon';
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
import { Skeleton } from 'mastodon/components/skeleton';
import { LinkFooter} from 'mastodon/features/ui/components/link_footer';
import { CapabilityIcon } from './components/capability_icon';
import { Section } from './components/section';
import { RulesSection } from './components/rules';
const messages = defineMessages({
title: { id: 'column.about', defaultMessage: 'About' },
rules: { id: 'about.rules', defaultMessage: 'Server rules' },
blocks: { id: 'about.blocks', defaultMessage: 'Moderated servers' },
fullTextSearch: { id: 'about.full_text_search', defaultMessage: 'Full text search' },
localTimeline: { id: 'column.community', defaultMessage: 'Local timeline' },
@ -66,67 +62,6 @@ const mapStateToProps = state => ({
domainBlocks: state.getIn(['server', 'domainBlocks']),
});
class Section extends PureComponent {
static propTypes = {
title: PropTypes.string,
children: PropTypes.node,
open: PropTypes.bool,
onOpen: PropTypes.func,
};
state = {
collapsed: !this.props.open,
};
handleClick = () => {
const { onOpen } = this.props;
const { collapsed } = this.state;
this.setState({ collapsed: !collapsed }, () => onOpen && onOpen());
};
render () {
const { title, children } = this.props;
const { collapsed } = this.state;
return (
<div className={classNames('about__section', { active: !collapsed })}>
<div className='about__section__title' role='button' tabIndex={0} onClick={this.handleClick}>
<Icon id={collapsed ? 'chevron-right' : 'chevron-down'} icon={collapsed ? ChevronRightIcon : ExpandMoreIcon} /> {title}
</div>
{!collapsed && (
<div className='about__section__body'>{children}</div>
)}
</div>
);
}
}
class CapabilityIcon extends PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
state: PropTypes.bool,
};
render () {
const { intl, state } = this.props;
if (state) {
return (
<span className='capability-icon enabled'><Icon id='check' icon={EnabledIcon} title={intl.formatMessage(messages.enabled)} />{intl.formatMessage(messages.enabled)}</span>
);
} else {
return (
<span className='capability-icon disabled'><Icon id='times' icon={DisabledIcon} title={intl.formatMessage(messages.disabled)} />{intl.formatMessage(messages.disabled)}</span>
);
}
}
}
class About extends PureComponent {
static propTypes = {
@ -215,23 +150,7 @@ class About extends PureComponent {
))}
</Section>
<Section title={intl.formatMessage(messages.rules)}>
{!isLoading && (server.get('rules', ImmutableList()).isEmpty() ? (
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
) : (
<ol className='rules-list'>
{server.get('rules').map(rule => {
const text = rule.getIn(['translations', locale, 'text']) || rule.getIn(['translations', locale.split('-')[0], 'text']) || rule.get('text');
const hint = rule.getIn(['translations', locale, 'hint']) || rule.getIn(['translations', locale.split('-')[0], 'hint']) || rule.get('hint');
return (
<li key={rule.get('id')}>
<div className='rules-list__text'>{text}</div>
{hint.length > 0 && (<div className='rules-list__hint'>{hint}</div>)}
</li>
)})}
</ol>
))}
</Section>
<RulesSection />
<Section title={intl.formatMessage(messages.capabilities)}>
<p><FormattedMessage id='about.kmyblue_capability' defaultMessage='This server is using kmyblue, a fork of Mastodon. On this server, kmyblues unique features are configured as follows.' /></p>

View file

@ -3,7 +3,6 @@
// emojiIndex.search functionality.
import type { BaseEmoji } from 'emoji-mart';
import type { Emoji } from 'emoji-mart/dist-es/utils/data';
// eslint-disable-next-line import/no-unresolved
import emojiCompressed from 'virtual:mastodon-emoji-compressed';
import type {
Search,

View file

@ -2,7 +2,6 @@
// (i.e. the svg filename) and a shortCode intended to be shown
// as a "title" attribute in an HTML element (aka tooltip).
// eslint-disable-next-line import/no-unresolved
import emojiCompressed from 'virtual:mastodon-emoji-compressed';
import type {
FilenameData,