Merge commit '055f581ca58b62132c1eff23453031d9df9dbf0e' into glitch-soc/merge-upstream
This commit is contained in:
commit
6ff4dad89d
101 files changed
+1507
-674
No files matched your search
@@ -32,16 +32,38 @@ interface Rule extends BaseRule {
|
||||
translations?: Record<string, BaseRule>;
|
||||
}
|
||||
|
||||
function getDefaultSelectedLocale(
|
||||
currentUiLocale: string,
|
||||
localeOptions: SelectItem[],
|
||||
) {
|
||||
const preciseMatch = localeOptions.find(
|
||||
(option) => option.value === currentUiLocale,
|
||||
);
|
||||
if (preciseMatch) {
|
||||
return preciseMatch.value;
|
||||
}
|
||||
|
||||
const partialLocale = currentUiLocale.split('-')[0];
|
||||
const partialMatch = localeOptions.find(
|
||||
(option) => option.value.split('-')[0] === partialLocale,
|
||||
);
|
||||
|
||||
return partialMatch?.value ?? 'default';
|
||||
}
|
||||
|
||||
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 [selectedLocale, setSelectedLocale] = useState(() =>
|
||||
getDefaultSelectedLocale(intl.locale, localeOptions),
|
||||
);
|
||||
const rules = useAppSelector((state) => rulesSelector(state, selectedLocale));
|
||||
|
||||
const handleLocaleChange: ChangeEventHandler<HTMLSelectElement> = useCallback(
|
||||
(e) => {
|
||||
setLocale(e.currentTarget.value);
|
||||
setSelectedLocale(e.currentTarget.value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
@@ -74,25 +96,27 @@ export const RulesSection: FC<RulesSectionProps> = ({ isLoading = false }) => {
|
||||
))}
|
||||
</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>
|
||||
{localeOptions.length > 1 && (
|
||||
<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 === selectedLocale}
|
||||
>
|
||||
{option.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -123,11 +123,10 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
canSubmit = () => {
|
||||
const { isSubmitting, isChangingUpload, isUploading, anyMedia, maxChars } = this.props;
|
||||
const { isSubmitting, isChangingUpload, isUploading, maxChars } = this.props;
|
||||
const fulltext = this.getFulltextForCharacterCounting();
|
||||
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
|
||||
|
||||
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars || (isOnlyWhitespace && !anyMedia));
|
||||
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars);
|
||||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
|
||||
+8
-8
@@ -15,24 +15,24 @@ export interface IAnnouncement extends ApiAnnouncementJSON {
|
||||
|
||||
interface AnnouncementProps {
|
||||
announcement: IAnnouncement;
|
||||
selected: boolean;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export const Announcement: FC<AnnouncementProps> = ({
|
||||
announcement,
|
||||
selected,
|
||||
active,
|
||||
}) => {
|
||||
const [unread, setUnread] = useState(!announcement.read);
|
||||
useEffect(() => {
|
||||
// Only update `unread` marker once the announcement is out of view
|
||||
if (!selected && unread !== !announcement.read) {
|
||||
if (!active && unread !== !announcement.read) {
|
||||
setUnread(!announcement.read);
|
||||
}
|
||||
}, [announcement.read, selected, unread]);
|
||||
}, [announcement.read, active, unread]);
|
||||
|
||||
return (
|
||||
<AnimateEmojiProvider className='announcements__item'>
|
||||
<strong className='announcements__item__range'>
|
||||
<AnimateEmojiProvider>
|
||||
<strong className='announcements__range'>
|
||||
<FormattedMessage
|
||||
id='announcement.announcement'
|
||||
defaultMessage='Announcement'
|
||||
@@ -44,14 +44,14 @@ export const Announcement: FC<AnnouncementProps> = ({
|
||||
</strong>
|
||||
|
||||
<EmojiHTML
|
||||
className='announcements__item__content translate'
|
||||
className='announcements__content translate'
|
||||
htmlString={announcement.contentHtml}
|
||||
extraEmojis={announcement.emojis}
|
||||
/>
|
||||
|
||||
<ReactionsBar reactions={announcement.reactions} id={announcement.id} />
|
||||
|
||||
{unread && <span className='announcements__item__unread' />}
|
||||
{unread && <span className='announcements__unread' />}
|
||||
</AnimateEmojiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,63 +1,50 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import type { Map, List } from 'immutable';
|
||||
|
||||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
|
||||
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
||||
import type { RenderSlideFn } from '@/mastodon/components/carousel';
|
||||
import { Carousel } from '@/mastodon/components/carousel';
|
||||
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||
import { IconButton } from '@/mastodon/components/icon_button';
|
||||
import { mascot, reduceMotion } from '@/mastodon/initial_state';
|
||||
import { mascot } from '@/mastodon/initial_state';
|
||||
import { createAppSelector, useAppSelector } from '@/mastodon/store';
|
||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
|
||||
import type { IAnnouncement } from './announcement';
|
||||
import { Announcement } from './announcement';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
|
||||
next: { id: 'lightbox.next', defaultMessage: 'Next' },
|
||||
});
|
||||
|
||||
const announcementSelector = createAppSelector(
|
||||
[(state) => state.announcements as Map<string, List<Map<string, unknown>>>],
|
||||
(announcements) =>
|
||||
(announcements.get('items')?.toJS() as IAnnouncement[] | undefined) ?? [],
|
||||
((announcements.get('items')?.toJS() as IAnnouncement[] | undefined) ?? [])
|
||||
.map((announcement) => ({ announcement, id: announcement.id }))
|
||||
.toReversed(),
|
||||
);
|
||||
|
||||
export const Announcements: FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const announcements = useAppSelector(announcementSelector);
|
||||
const emojis = useAppSelector((state) => state.custom_emojis);
|
||||
|
||||
const [index, setIndex] = useState(0);
|
||||
const handleChangeIndex = useCallback(
|
||||
(idx: number) => {
|
||||
setIndex(idx % announcements.length);
|
||||
},
|
||||
[announcements.length],
|
||||
const renderSlide: RenderSlideFn<{
|
||||
id: string;
|
||||
announcement: IAnnouncement;
|
||||
}> = useCallback(
|
||||
(item, active) => (
|
||||
<Announcement
|
||||
announcement={item.announcement}
|
||||
active={active}
|
||||
key={item.id}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
const handleNextIndex = useCallback(() => {
|
||||
setIndex((prevIndex) => (prevIndex + 1) % announcements.length);
|
||||
}, [announcements.length]);
|
||||
const handlePrevIndex = useCallback(() => {
|
||||
setIndex((prevIndex) =>
|
||||
prevIndex === 0 ? announcements.length - 1 : prevIndex - 1,
|
||||
);
|
||||
}, [announcements.length]);
|
||||
|
||||
if (announcements.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='announcements'>
|
||||
<div className='announcements__root'>
|
||||
<img
|
||||
className='announcements__mastodon'
|
||||
alt=''
|
||||
@@ -65,48 +52,13 @@ export const Announcements: FC = () => {
|
||||
src={mascot ?? elephantUIPlane}
|
||||
/>
|
||||
|
||||
<div className='announcements__container'>
|
||||
<CustomEmojiProvider emojis={emojis}>
|
||||
<ReactSwipeableViews
|
||||
animateHeight
|
||||
animateTransitions={!reduceMotion}
|
||||
index={index}
|
||||
onChangeIndex={handleChangeIndex}
|
||||
>
|
||||
{announcements
|
||||
.map((announcement, idx) => (
|
||||
<Announcement
|
||||
key={announcement.id}
|
||||
announcement={announcement}
|
||||
selected={index === idx}
|
||||
/>
|
||||
))
|
||||
.reverse()}
|
||||
</ReactSwipeableViews>
|
||||
</CustomEmojiProvider>
|
||||
|
||||
{announcements.length > 1 && (
|
||||
<div className='announcements__pagination'>
|
||||
<IconButton
|
||||
disabled={announcements.length === 1}
|
||||
title={intl.formatMessage(messages.previous)}
|
||||
icon='chevron-left'
|
||||
iconComponent={ChevronLeftIcon}
|
||||
onClick={handlePrevIndex}
|
||||
/>
|
||||
<span>
|
||||
{index + 1} / {announcements.length}
|
||||
</span>
|
||||
<IconButton
|
||||
disabled={announcements.length === 1}
|
||||
title={intl.formatMessage(messages.next)}
|
||||
icon='chevron-right'
|
||||
iconComponent={ChevronRightIcon}
|
||||
onClick={handleNextIndex}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<CustomEmojiProvider emojis={emojis}>
|
||||
<Carousel
|
||||
classNamePrefix='announcements'
|
||||
renderItem={renderSlide}
|
||||
items={announcements}
|
||||
/>
|
||||
</CustomEmojiProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -21,7 +21,10 @@ export const LinkFooter: React.FC<{
|
||||
<p>
|
||||
<strong>{domain}</strong>:{' '}
|
||||
<Link to='/about' target={multiColumn ? '_blank' : undefined}>
|
||||
<FormattedMessage id='footer.about' defaultMessage='About' />
|
||||
<FormattedMessage
|
||||
id='footer.about_this_server'
|
||||
defaultMessage='About'
|
||||
/>
|
||||
</Link>
|
||||
{statusPageUrl && (
|
||||
<>
|
||||
|
||||
Reference in new issue
Block a user