Merge remote-tracking branch 'parent/main' into kbtopic-remove-quote
This commit is contained in:
commit
c538c23ef7
141 changed files with 1552 additions and 779 deletions
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
156
app/javascript/mastodon/features/about/components/rules.tsx
Normal file
156
app/javascript/mastodon/features/about/components/rules.tsx
Normal 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);
|
||||
},
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue