1
0
Fork 0
forked from gitea/nas

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>
);
};