mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-14 08:19:05 +00:00
[Glitch] Add language picker to server rules section
Port d78535eab9 to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
@@ -18,7 +18,7 @@ export interface SelectItem {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
iconComponent?: IconProp;
|
iconComponent?: IconProp;
|
||||||
text: string;
|
text: string;
|
||||||
meta: string;
|
meta?: string;
|
||||||
extra?: string;
|
extra?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 '@/flavours/glitch/components/dropdown_selector';
|
||||||
|
import type { RootState } from '@/flavours/glitch/store';
|
||||||
|
import { useAppSelector } from '@/flavours/glitch/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 '@/flavours/glitch/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,26 +3,23 @@ import { PureComponent } from 'react';
|
|||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
|
||||||
import ExpandMoreIcon from '@/material-icons/400-24px/expand_more.svg?react';
|
|
||||||
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server';
|
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server';
|
||||||
import { Account } from 'flavours/glitch/components/account';
|
import { Account } from 'flavours/glitch/components/account';
|
||||||
import Column from 'flavours/glitch/components/column';
|
import Column from 'flavours/glitch/components/column';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
|
||||||
import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image';
|
import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image';
|
||||||
import { Skeleton } from 'flavours/glitch/components/skeleton';
|
import { Skeleton } from 'flavours/glitch/components/skeleton';
|
||||||
import { LinkFooter} from 'flavours/glitch/features/ui/components/link_footer';
|
import { LinkFooter} from 'flavours/glitch/features/ui/components/link_footer';
|
||||||
|
|
||||||
|
import { Section } from './components/section';
|
||||||
|
import { RulesSection } from './components/rules';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.about', defaultMessage: 'About' },
|
title: { id: 'column.about', defaultMessage: 'About' },
|
||||||
rules: { id: 'about.rules', defaultMessage: 'Server rules' },
|
|
||||||
blocks: { id: 'about.blocks', defaultMessage: 'Moderated servers' },
|
blocks: { id: 'about.blocks', defaultMessage: 'Moderated servers' },
|
||||||
silenced: { id: 'about.domain_blocks.silenced.title', defaultMessage: 'Limited' },
|
silenced: { id: 'about.domain_blocks.silenced.title', defaultMessage: 'Limited' },
|
||||||
silencedExplanation: { id: 'about.domain_blocks.silenced.explanation', defaultMessage: 'You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.' },
|
silencedExplanation: { id: 'about.domain_blocks.silenced.explanation', defaultMessage: 'You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.' },
|
||||||
@@ -49,45 +46,6 @@ const mapStateToProps = state => ({
|
|||||||
domainBlocks: state.getIn(['server', 'domainBlocks']),
|
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 About extends PureComponent {
|
class About extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -165,23 +123,7 @@ class About extends PureComponent {
|
|||||||
))}
|
))}
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title={intl.formatMessage(messages.rules)}>
|
<RulesSection />
|
||||||
{!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>
|
|
||||||
|
|
||||||
<Section title={intl.formatMessage(messages.blocks)} onOpen={this.handleDomainBlocksOpen}>
|
<Section title={intl.formatMessage(messages.blocks)} onOpen={this.handleDomainBlocksOpen}>
|
||||||
{domainBlocks.get('isLoading') ? (
|
{domainBlocks.get('isLoading') ? (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@use 'variables' as *;
|
@use 'variables' as *;
|
||||||
|
@use 'functions' as *;
|
||||||
|
|
||||||
$maximum-width: 1235px;
|
$maximum-width: 1235px;
|
||||||
$fluid-breakpoint: $maximum-width + 20px;
|
$fluid-breakpoint: $maximum-width + 20px;
|
||||||
@@ -93,3 +94,52 @@ $fluid-breakpoint: $maximum-width + 20px;
|
|||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rules-languages {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
> select {
|
||||||
|
appearance: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $primary-text-color;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
outline: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
resize: vertical;
|
||||||
|
background: $ui-base-color;
|
||||||
|
border: 1px solid var(--background-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding-inline-start: 10px;
|
||||||
|
padding-inline-end: 30px;
|
||||||
|
height: 41px;
|
||||||
|
|
||||||
|
@media screen and (width <= 600px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
content: '';
|
||||||
|
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14.933' height='18.467' viewBox='0 0 14.933 18.467'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='currentColor' /></svg>")
|
||||||
|
no-repeat 50% 50%;
|
||||||
|
mask-size: contain;
|
||||||
|
right: 8px;
|
||||||
|
background-color: lighten($ui-base-color, 12%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10157,6 +10157,8 @@ noscript {
|
|||||||
border: 1px solid var(--background-border-color);
|
border: 1px solid var(--background-border-color);
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active &__title {
|
&.active &__title {
|
||||||
|
|||||||
Reference in New Issue
Block a user