mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-13 15:58:50 +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;
|
||||
iconComponent?: IconProp;
|
||||
text: string;
|
||||
meta: string;
|
||||
meta?: 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 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 ExpandMoreIcon from '@/material-icons/400-24px/expand_more.svg?react';
|
||||
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server';
|
||||
import { Account } from 'flavours/glitch/components/account';
|
||||
import Column from 'flavours/glitch/components/column';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image';
|
||||
import { Skeleton } from 'flavours/glitch/components/skeleton';
|
||||
import { LinkFooter} from 'flavours/glitch/features/ui/components/link_footer';
|
||||
|
||||
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' },
|
||||
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.' },
|
||||
@@ -49,45 +46,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 About extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -165,23 +123,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.blocks)} onOpen={this.handleDomainBlocksOpen}>
|
||||
{domainBlocks.get('isLoading') ? (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@use 'variables' as *;
|
||||
@use 'functions' as *;
|
||||
|
||||
$maximum-width: 1235px;
|
||||
$fluid-breakpoint: $maximum-width + 20px;
|
||||
@@ -93,3 +94,52 @@ $fluid-breakpoint: $maximum-width + 20px;
|
||||
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);
|
||||
color: $highlight-text-color;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
&.active &__title {
|
||||
|
||||
Reference in New Issue
Block a user