From ebab5e42c0be8cbffcd0bb3f2280d02f7bdcc56f Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 27 May 2025 15:57:34 +0200 Subject: [PATCH] [Glitch] Add language picker to server rules section Port d78535eab9cefc285c0d7ef88adb125ab1ceb6bd to glitch-soc Signed-off-by: Claire --- .../glitch/components/dropdown_selector.tsx | 2 +- .../features/about/components/rules.tsx | 156 ++++++++++++++++++ .../features/about/components/section.tsx | 45 +++++ .../flavours/glitch/features/about/index.jsx | 66 +------- .../flavours/glitch/styles/about.scss | 50 ++++++ .../flavours/glitch/styles/components.scss | 2 + 6 files changed, 258 insertions(+), 63 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/about/components/rules.tsx create mode 100644 app/javascript/flavours/glitch/features/about/components/section.tsx diff --git a/app/javascript/flavours/glitch/components/dropdown_selector.tsx b/app/javascript/flavours/glitch/components/dropdown_selector.tsx index b86d2d0f80..99bbd182e5 100644 --- a/app/javascript/flavours/glitch/components/dropdown_selector.tsx +++ b/app/javascript/flavours/glitch/components/dropdown_selector.tsx @@ -18,7 +18,7 @@ export interface SelectItem { icon?: string; iconComponent?: IconProp; text: string; - meta: string; + meta?: string; extra?: string; } diff --git a/app/javascript/flavours/glitch/features/about/components/rules.tsx b/app/javascript/flavours/glitch/features/about/components/rules.tsx new file mode 100644 index 0000000000..2ba95fedb3 --- /dev/null +++ b/app/javascript/flavours/glitch/features/about/components/rules.tsx @@ -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; +} + +export const RulesSection: FC = ({ 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 = useCallback( + (e) => { + setLocale(e.currentTarget.value); + }, + [], + ); + + if (isLoading) { + return
; + } + + if (rules.length === 0) { + return ( +
+

+ +

+
+ ); + } + + return ( +
+
    + {rules.map((rule) => ( +
  1. +
    {rule.text}
    + {!!rule.hint &&
    {rule.hint}
    } +
  2. + ))} +
+ +
+ + +
+
+ ); +}; + +const selectRules = (state: RootState) => { + const rules = state.server.getIn([ + 'server', + 'rules', + ]) as ImmutableList | 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 = { + 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); + }, +); diff --git a/app/javascript/flavours/glitch/features/about/components/section.tsx b/app/javascript/flavours/glitch/features/about/components/section.tsx new file mode 100644 index 0000000000..39ebf65c2d --- /dev/null +++ b/app/javascript/flavours/glitch/features/about/components/section.tsx @@ -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 = ({ + title, + children, + open = false, + onOpen, +}) => { + const [collapsed, setCollapsed] = useState(!open); + const handleClick: MouseEventHandler = useCallback(() => { + setCollapsed((prev) => !prev); + onOpen?.(); + }, [onOpen]); + return ( +
+ + + {!collapsed &&
{children}
} +
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/about/index.jsx b/app/javascript/flavours/glitch/features/about/index.jsx index 2b0c356acb..5bab3add59 100644 --- a/app/javascript/flavours/glitch/features/about/index.jsx +++ b/app/javascript/flavours/glitch/features/about/index.jsx @@ -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 ( -
-
- {title} -
- - {!collapsed && ( -
{children}
- )} -
- ); - } - -} - class About extends PureComponent { static propTypes = { @@ -165,23 +123,7 @@ class About extends PureComponent { ))}
-
- {!isLoading && (server.get('rules', ImmutableList()).isEmpty() ? ( -

- ) : ( -
    - {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 ( -
  1. -
    {text}
    - {hint.length > 0 && (
    {hint}
    )} -
  2. - )})} -
- ))} -
+
{domainBlocks.get('isLoading') ? ( diff --git a/app/javascript/flavours/glitch/styles/about.scss b/app/javascript/flavours/glitch/styles/about.scss index 9a13034a3a..ba0605b79e 100644 --- a/app/javascript/flavours/glitch/styles/about.scss +++ b/app/javascript/flavours/glitch/styles/about.scss @@ -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,") + no-repeat 50% 50%; + mask-size: contain; + right: 8px; + background-color: lighten($ui-base-color, 12%); + pointer-events: none; + } +} diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index 82e152e5c3..a117a2b511 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -10157,6 +10157,8 @@ noscript { border: 1px solid var(--background-border-color); color: $highlight-text-color; cursor: pointer; + width: 100%; + background: none; } &.active &__title {