+
diff --git a/app/javascript/flavours/glitch/features/custom_homepage/about.tsx b/app/javascript/flavours/glitch/features/custom_homepage/about.tsx
new file mode 100644
index 0000000000..2d690c0709
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/custom_homepage/about.tsx
@@ -0,0 +1,75 @@
+import { useEffect } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { fetchExtendedDescription } from 'flavours/glitch/actions/server';
+import { Account } from 'flavours/glitch/components/account';
+import { Skeleton } from 'flavours/glitch/components/skeleton';
+import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
+
+import classes from './styles.module.scss';
+
+const Placeholder = () => (
+
+
+
+
+
+);
+
+export const About = () => {
+ const dispatch = useAppDispatch();
+ const server = useAppSelector((state) => state.server.server);
+ const extendedDescription = useAppSelector(
+ (state) => state.server.extendedDescription,
+ );
+
+ const accountId = server.item?.contact.account?.id ?? '';
+ const isLoading = extendedDescription.isLoading;
+ const hasContent = (extendedDescription.item?.content.length ?? 0) > 0;
+ const content = extendedDescription.item?.content ?? '';
+
+ useEffect(() => {
+ void dispatch(fetchExtendedDescription());
+ }, [dispatch]);
+
+ return (
+ <>
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : hasContent ? (
+
+ ) : (
+
+ )}
+
+ >
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/custom_homepage/components/footer.tsx b/app/javascript/flavours/glitch/features/custom_homepage/components/footer.tsx
new file mode 100644
index 0000000000..6d437d4043
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/custom_homepage/components/footer.tsx
@@ -0,0 +1,39 @@
+import { useEffect } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import { fetchServer } from 'flavours/glitch/actions/server';
+import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
+
+import classes from '../styles.module.scss';
+
+export const Footer = () => {
+ const dispatch = useAppDispatch();
+ const server = useAppSelector((state) => state.server.server);
+ const email = server.item?.contact.email ?? '';
+
+ useEffect(() => {
+ void dispatch(fetchServer());
+ }, [dispatch]);
+
+ return (
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/custom_homepage/components/header.tsx b/app/javascript/flavours/glitch/features/custom_homepage/components/header.tsx
new file mode 100644
index 0000000000..3af673da2a
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/custom_homepage/components/header.tsx
@@ -0,0 +1,25 @@
+import { FormattedMessage } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import { domain, sso_redirect } from 'flavours/glitch/initial_state';
+
+import classes from '../styles.module.scss';
+
+export const Header = () => (
+
+);
diff --git a/app/javascript/flavours/glitch/features/custom_homepage/index.tsx b/app/javascript/flavours/glitch/features/custom_homepage/index.tsx
new file mode 100644
index 0000000000..ef64a82c70
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/custom_homepage/index.tsx
@@ -0,0 +1,71 @@
+import { useEffect } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { Route, Switch, useRouteMatch } from 'react-router-dom';
+
+import { Helmet } from '@unhead/react/helmet';
+
+import { fetchServer } from 'flavours/glitch/actions/server';
+import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image';
+import { TabLink, TabList } from 'flavours/glitch/components/tab_list';
+import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
+
+import { About } from './about';
+import { LatestActivity } from './latest_activity';
+import classes from './styles.module.scss';
+
+export const CustomHomepage: React.FC = () => {
+ const dispatch = useAppDispatch();
+ const server = useAppSelector((state) => state.server.server);
+ const { path } = useRouteMatch();
+
+ useEffect(() => {
+ void dispatch(fetchServer());
+ }, [dispatch]);
+
+ return (
+
+
+ `${server.item?.thumbnail.versions?.[key]} ${key.replace('@', '')}`,
+ )
+ .join(', ')}
+ className={classes.header}
+ />
+
+
+
{server.item?.domain}
+
{server.item?.description}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {server.item?.domain}
+
+
+
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/custom_homepage/latest_activity.tsx b/app/javascript/flavours/glitch/features/custom_homepage/latest_activity.tsx
new file mode 100644
index 0000000000..23f076e352
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/custom_homepage/latest_activity.tsx
@@ -0,0 +1,35 @@
+import { useEffect } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { expandCommunityTimeline } from 'flavours/glitch/actions/timelines';
+import { Callout } from 'flavours/glitch/components/callout';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
+import { useAppDispatch } from 'flavours/glitch/store';
+
+import classes from './styles.module.scss';
+
+export const LatestActivity = () => {
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ void dispatch(expandCommunityTimeline());
+ }, [dispatch]);
+
+ return (
+
+
+
+ }
+ scrollKey='custom_homepage'
+ timelineId='community'
+ maxItems={40}
+ bindToDocument
+ />
+ );
+};
diff --git a/app/javascript/flavours/glitch/features/custom_homepage/styles.module.scss b/app/javascript/flavours/glitch/features/custom_homepage/styles.module.scss
new file mode 100644
index 0000000000..bbf834ffc9
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/custom_homepage/styles.module.scss
@@ -0,0 +1,145 @@
+.page {
+ border-radius: 16px;
+ border: 1px solid var(--color-border-primary);
+ background: var(--color-bg-primary);
+ min-height: 100%;
+
+ :global(.item-list) article:last-child :global(.status) {
+ border-bottom: 0;
+ }
+
+ @media screen and (width <= 1175px) {
+ border-radius: 0;
+ }
+}
+
+.header {
+ aspect-ratio: 40/21;
+ border-radius: 16px 16px 0 0;
+
+ @media screen and (width <= 1175px) {
+ border-radius: 0;
+ }
+}
+
+.banner {
+ margin: 16px;
+ margin-bottom: 0;
+}
+
+.topSection {
+ display: flex;
+ padding: 16px;
+ flex-direction: column;
+ gap: 8px;
+ color: var(--color-text-primary);
+
+ h1 {
+ font-size: 24px;
+ font-weight: 500;
+ line-height: 30px;
+ letter-spacing: -0.12px;
+ }
+
+ p {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ font-size: 16px;
+ line-height: 24px;
+ text-overflow: ellipsis;
+ }
+}
+
+.block {
+ padding: 16px;
+
+ h2 {
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 22.4px;
+ margin-bottom: 8px;
+ }
+
+ :global(.account) {
+ border: 1px solid var(--color-border-primary);
+ padding: 18px 16px;
+ border-radius: 12px;
+
+ --avatar-border-radius: 50%;
+ }
+}
+
+.placeholder {
+ padding: 4px 0;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+
+ :global(.skeleton) {
+ height: 40px;
+ border-radius: 12px;
+ background: var(--color-bg-overlay-highlight);
+ }
+}
+
+.minimalHeader {
+ padding-top: 24px;
+ padding-bottom: 12px;
+ display: flex;
+ align-items: center;
+
+ @media screen and (width <= 1175px) {
+ padding-inline-start: 12px;
+ padding-inline-end: 12px;
+ }
+}
+
+.leftSide {
+ font-size: 15px;
+ font-weight: 600;
+ max-width: 300px;
+
+ a {
+ text-decoration: none;
+ color: inherit;
+ }
+}
+
+.rightSide {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 4px;
+ flex: 1 0 0;
+}
+
+.minimalFooter {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ color: var(--color-text-secondary);
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 22.4px;
+ padding-top: 28px;
+ padding-bottom: 54px;
+ gap: 8px;
+ flex-wrap: wrap;
+
+ a {
+ color: inherit;
+ text-decoration: underline;
+ }
+
+ @media screen and (width <= 1175px) {
+ justify-content: center;
+ padding-inline-start: 12px;
+ padding-inline-end: 12px;
+ }
+}
+
+.contact {
+ display: flex;
+ gap: 8px;
+}
diff --git a/app/javascript/flavours/glitch/features/emoji/database.ts b/app/javascript/flavours/glitch/features/emoji/database.ts
index b1cbf3ac0a..f95507cb75 100644
--- a/app/javascript/flavours/glitch/features/emoji/database.ts
+++ b/app/javascript/flavours/glitch/features/emoji/database.ts
@@ -85,13 +85,16 @@ export async function search({
// Only query the range for the last token to allow partial matches.
const range =
i === queryTokens.length - 1
- ? IDBKeyRange.bound(token, token + '\uffff')
+ ? IDBKeyRange.lowerBound(token)
: IDBKeyRange.only(token);
- const [unicodeResults, customResults] = await Promise.all([
- db.getAllFromIndex(locale, 'tokens', range),
- db.getAllFromIndex('custom', 'tokens', range),
- ]);
+ const [unicodeResults, customResults, shortcodeResults] = await Promise.all(
+ [
+ db.getAllFromIndex(locale, 'tokens', range),
+ db.getAllFromIndex('custom', 'tokens', range),
+ db.getAllFromIndex('shortcodes', 'shortcodes', range),
+ ],
+ );
const resultMap: ScoreMap = new Map();
for (const emoji of unicodeResults) {
const score = getScoreForEmoji(emoji, token);
@@ -107,6 +110,22 @@ export async function search({
}
resultMap.set(emoji.shortcode, { ...emoji, score });
}
+
+ for (const shortcodeResult of shortcodeResults) {
+ if (resultMap.has(shortcodeResult.hexcode)) {
+ continue;
+ }
+ const emoji = await db.get(locale, shortcodeResult.hexcode);
+ if (!emoji) {
+ continue;
+ }
+ const score = getScoreForEmoji(emoji, token);
+ if (score === null) {
+ continue;
+ }
+ resultMap.set(emoji.hexcode, { ...emoji, score });
+ }
+
log('found %d results for token "%s"', resultMap.size, token);
resultArrays.push(resultMap);
}
@@ -147,7 +166,7 @@ function getScoreForEmoji(emoji: AnyEmojiData, query: string) {
}
let index = 1;
- for (const token of [id, emoji.tokens]) {
+ for (const token of [id, ...emoji.tokens]) {
const tokenIndex = token.indexOf(query);
if (tokenIndex !== -1) {
return index + tokenIndex / token.length;
diff --git a/app/javascript/flavours/glitch/features/emoji/index.ts b/app/javascript/flavours/glitch/features/emoji/index.ts
index 7714232605..f9614e15ec 100644
--- a/app/javascript/flavours/glitch/features/emoji/index.ts
+++ b/app/javascript/flavours/glitch/features/emoji/index.ts
@@ -2,6 +2,7 @@ import { initialState } from '@/flavours/glitch/initial_state';
import type { EMOJI_DB_NAME_SHORTCODES } from './constants';
import { toSupportedLocale } from './locale';
+import { reloadCustomEmojis } from './picker';
import type { LocaleOrCustom } from './types';
import { emojiLogger } from './utils';
@@ -90,6 +91,7 @@ export async function loadCustomEmoji() {
const emojis = await importCustomEmojiData();
if (emojis && emojis.length > 0) {
log('loaded %d custom emojis', emojis.length);
+ await reloadCustomEmojis();
}
}
}
diff --git a/app/javascript/flavours/glitch/features/emoji/normalize.ts b/app/javascript/flavours/glitch/features/emoji/normalize.ts
index 2816f6125c..6c3cf9d159 100644
--- a/app/javascript/flavours/glitch/features/emoji/normalize.ts
+++ b/app/javascript/flavours/glitch/features/emoji/normalize.ts
@@ -14,6 +14,7 @@ import {
EMOJIS_REQUIRING_INVERSION_IN_DARK_MODE,
EMOJI_MIN_TOKEN_LENGTH,
} from './constants';
+import { localeToSegmenter } from './locale';
import type {
CustomEmojiData,
CustomEmojiMapArg,
@@ -92,10 +93,11 @@ export function transformEmojiData(
export function transformCustomEmojiData(
emoji: ApiCustomEmojiJSON,
): CustomEmojiData {
- const tokens = emoji.shortcode
- .split('_')
- .filter((word) => word.length >= EMOJI_MIN_TOKEN_LENGTH)
- .map((word) => word.toLowerCase());
+ const tokens = extractTokens(emoji.shortcode, localeToSegmenter('en'));
+ if (!tokens.includes(emoji.shortcode)) {
+ tokens.unshift(emoji.shortcode);
+ }
+
return {
...emoji,
tokens,
@@ -215,7 +217,9 @@ export function extractTokens(
// Prefer to use Intl.Segmenter if available for better locale support.
if (segmenter) {
for (const { isWordLike, segment } of segmenter.segment(
- input.replaceAll('_', ' '), // Handle underscores from shortcodes.
+ input
+ .replaceAll(/[_-]+/g, ' ') // Handle underscores from shortcodes.
+ .replaceAll(/([a-z])([A-Z])/g, '$1 $2'), // Handle camelCase.
)) {
if (isWordLike && segment.length >= EMOJI_MIN_TOKEN_LENGTH) {
tokens.push(segment.toLowerCase());
diff --git a/app/javascript/flavours/glitch/features/emoji/picker.ts b/app/javascript/flavours/glitch/features/emoji/picker.ts
index 772d927f2b..c2a97cf969 100644
--- a/app/javascript/flavours/glitch/features/emoji/picker.ts
+++ b/app/javascript/flavours/glitch/features/emoji/picker.ts
@@ -82,13 +82,22 @@ type LegacyEmoji =
custom: true;
};
+export async function reloadCustomEmojis() {
+ customEmojis = null;
+
+ const { loadEmojisIntoCache } =
+ await import('@/flavours/glitch/hooks/useCustomEmojis');
+
+ await Promise.all([fetchCustomEmojiData(), loadEmojisIntoCache()]);
+}
+
// Replicates the old legacy search function.
export async function emojiMartSearch(
token: string,
locale: string,
limit = 5,
): Promise {
- const query = token.replace(':', '').toLowerCase().trim();
+ const query = token.replace(':', '').trim();
if (!query.length) {
return [];
}
diff --git a/app/javascript/flavours/glitch/features/getting_started/index.tsx b/app/javascript/flavours/glitch/features/getting_started/index.tsx
index 73a837969e..4858a48d30 100644
--- a/app/javascript/flavours/glitch/features/getting_started/index.tsx
+++ b/app/javascript/flavours/glitch/features/getting_started/index.tsx
@@ -4,14 +4,16 @@ import { Helmet } from '@unhead/react/helmet';
import { Column } from 'flavours/glitch/components/column';
-import { NavigationPanel } from '../navigation_panel';
+import { NavigationPanel, messages } from '../navigation_panel';
import { LinkFooter } from '../ui/components/link_footer';
const GettingStarted: React.FC = () => {
const intl = useIntl();
return (
-
+
diff --git a/app/javascript/flavours/glitch/features/navigation_panel/index.tsx b/app/javascript/flavours/glitch/features/navigation_panel/index.tsx
index 8305f5d6c5..39b9adc6a1 100644
--- a/app/javascript/flavours/glitch/features/navigation_panel/index.tsx
+++ b/app/javascript/flavours/glitch/features/navigation_panel/index.tsx
@@ -65,7 +65,7 @@ import { MoreLink } from './components/more_link';
import { SignInBanner } from './components/sign_in_banner';
import { Trends } from './components/trends';
-const messages = defineMessages({
+export const messages = defineMessages({
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
notifications: {
id: 'tabs_bar.notifications',
@@ -77,6 +77,12 @@ const messages = defineMessages({
id: 'column.firehose_singular',
defaultMessage: 'Live feed',
},
+ main: {
+ id: 'navigation_bar.main',
+ defaultMessage: 'Main',
+ description:
+ 'Label for the main navigation; should not contain the word "navigation".',
+ },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
@@ -455,6 +461,7 @@ export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({
};
export const CollapsibleNavigationPanel: React.FC = () => {
+ const intl = useIntl();
const open = useAppSelector((state) => state.navigation.open);
const dispatch = useAppDispatch();
const openable = useBreakpoint('openable');
@@ -563,7 +570,8 @@ export const CollapsibleNavigationPanel: React.FC = () => {
const showOverlay = openable && open;
return (
- {
>
-
+
);
};
diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.tsx b/app/javascript/flavours/glitch/features/ui/components/columns_area.tsx
index 818fef2e02..7c0c03cf0b 100644
--- a/app/javascript/flavours/glitch/features/ui/components/columns_area.tsx
+++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.tsx
@@ -12,6 +12,8 @@ import classNames from 'classnames';
import type { List, Record } from 'immutable';
import { useAppSelector } from '@/flavours/glitch/store';
+import { Footer } from 'flavours/glitch/features/custom_homepage/components/footer';
+import { Header } from 'flavours/glitch/features/custom_homepage/components/header';
import { CollapsibleNavigationPanel } from 'flavours/glitch/features/navigation_panel';
import { useBreakpoint } from '../hooks/useBreakpoint';
@@ -85,9 +87,10 @@ export const ColumnsArea = forwardRef<
HTMLDivElement,
{
singleColumn?: boolean;
+ minimalShell?: boolean;
children: React.ReactElement | React.ReactElement[];
}
->(({ children, singleColumn }, ref) => {
+>(({ children, minimalShell, singleColumn }, ref) => {
const renderComposePanel = !useBreakpoint('full');
const columns = useAppSelector((state) =>
(state.settings as Record<{ columns: List> }>).get(
@@ -98,6 +101,24 @@ export const ColumnsArea = forwardRef<
(state) => !state.modal.get('stack').isEmpty(),
);
+ if (minimalShell) {
+ return (
+
+
+
+
+
+
+
+
+
{children}
+
+
+
+
+ );
+ }
+
if (singleColumn) {
return (
@@ -108,12 +129,13 @@ export const ColumnsArea = forwardRef<
-
+
@@ -121,7 +143,7 @@ export const ColumnsArea = forwardRef<
}
return (
-
-
+
);
});
diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.module.scss b/app/javascript/flavours/glitch/features/ui/components/link_footer.module.scss
index f2b094744e..beb0173ffd 100644
--- a/app/javascript/flavours/glitch/features/ui/components/link_footer.module.scss
+++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.module.scss
@@ -41,6 +41,10 @@
&:not(:last-child)::after {
content: ' · ';
+
+ @supports (content: 'x' / 'y') {
+ content: ' · ' / '';
+ }
}
}
diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_bar.tsx b/app/javascript/flavours/glitch/features/ui/components/navigation_bar.tsx
index 5410f015d4..69a18756af 100644
--- a/app/javascript/flavours/glitch/features/ui/components/navigation_bar.tsx
+++ b/app/javascript/flavours/glitch/features/ui/components/navigation_bar.tsx
@@ -31,6 +31,10 @@ export const messages = defineMessages({
defaultMessage: 'Notifications',
},
menu: { id: 'tabs_bar.menu', defaultMessage: 'Menu' },
+ advancedUiQuickLinks: {
+ id: 'tabs_bar.quick_links',
+ defaultMessage: 'Quick links',
+ },
});
const IconLabelButton: React.FC<{
diff --git a/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js
index f485119d92..bdc6837dd0 100644
--- a/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js
+++ b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js
@@ -24,7 +24,15 @@ const getRegex = createSelector([
const makeGetStatusIds = (pending = false) => createSelector([
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
- (state, { type }) => state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()),
+ (state, { type, maxItems }) => {
+ const items = state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList());
+
+ if (maxItems) {
+ return items.take(maxItems);
+ }
+
+ return items;
+ },
(state) => state.get('statuses'),
getRegex,
], (columnSettings, statusIds, statuses, regex) => {
@@ -64,8 +72,17 @@ const makeMapStateToProps = () => {
const getStatusIds = makeGetStatusIds();
const getPendingStatusIds = makeGetStatusIds(true);
- const mapStateToProps = (state, { timelineId, regex, initialLoadingState = true }) => ({
- statusIds: getStatusIds(state, { type: timelineId, regex }),
+ /**
+ * @param {import('flavours/glitch/store').RootState} state
+ * @param {Object} props
+ * @param {string} props.timelineId
+ * @param {boolean} [props.initialLoadingState]
+ * @param {number} [props.maxItems]
+ * @param {RegExp} [props.regex]
+ */
+ const mapStateToProps = (state, { timelineId, regex, initialLoadingState = true, maxItems }) => ({
+ statusIds: getStatusIds(state, { type: timelineId, regex, maxItems }),
+
lastId: state.getIn(['timelines', timelineId, 'items'])?.last(),
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], initialLoadingState),
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx
index 2e8dab90bc..41329d3196 100644
--- a/app/javascript/flavours/glitch/features/ui/index.jsx
+++ b/app/javascript/flavours/glitch/features/ui/index.jsx
@@ -32,7 +32,7 @@ import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../act
import { clearHeight } from '../../actions/height_cache';
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
import { expandHomeTimeline } from '../../actions/timelines';
-import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards } from '../../initial_state';
+import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards, domain } from '../../initial_state';
import BundleColumnError from './components/bundle_column_error';
import { NavigationBar } from './components/navigation_bar';
@@ -91,6 +91,7 @@ import {
import { ColumnsContextProvider } from './util/columns_context';
import { focusColumn, getFocusedItemIndex, focusItemSibling, focusFirstItem } from './util/focusUtils';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
+import { CustomHomepage } from 'flavours/glitch/features/custom_homepage';
// Dummy import, to make sure that
ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles.
@@ -107,7 +108,7 @@ const mapStateToProps = state => ({
hasComposingContents: state.getIn(['compose', 'text']).trim().length !== 0 || state.getIn(['compose', 'media_attachments']).size > 0 || state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'quoted_status_id']) !== null,
canUploadMore:
!state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type')))
- && state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']),
+ && state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'item', 'configuration', 'statuses', 'max_media_attachments']),
isUploadEnabled:
state.getIn(['compose', 'isDragDisabled']) !== true,
isWide: state.getIn(['local_settings', 'stretch']),
@@ -185,13 +186,15 @@ class SwitchingColumnsArea extends PureComponent {
rootRedirect = '/explore';
} else if (localLiveFeedAccess === 'public' && landingPage === 'local_feed') {
rootRedirect = '/public/local';
+ } else if (landingPage === 'overview') {
+ rootRedirect = '/overview';
} else {
rootRedirect = '/about';
}
return (
-
+
@@ -270,6 +273,8 @@ class SwitchingColumnsArea extends PureComponent {
+
+
@@ -686,13 +691,18 @@ class UI extends PureComponent {
cheat: this.handleDonate,
};
+ const minimalShell = !this.props.identity.signedIn && landingPage === 'overview';
+
return (
-
+ {!minimalShell && (
+
+ )}
+
{moved && (
-
+ {!minimalShell && }
{layout !== 'mobile' && }
{!disableHoverCards && }
diff --git a/app/javascript/flavours/glitch/hooks/useCustomEmojis.ts b/app/javascript/flavours/glitch/hooks/useCustomEmojis.ts
index aeb77620dd..6eb3c17fbf 100644
--- a/app/javascript/flavours/glitch/hooks/useCustomEmojis.ts
+++ b/app/javascript/flavours/glitch/hooks/useCustomEmojis.ts
@@ -20,7 +20,7 @@ export function useCustomEmojis() {
return emojis;
}
-async function loadEmojisIntoCache() {
+export async function loadEmojisIntoCache() {
const { loadAllCustomEmoji } = await import('../features/emoji/database');
const emojisRaw = await loadAllCustomEmoji();
if (emojisRaw === null) {
diff --git a/app/javascript/flavours/glitch/styles/mastodon/admin.scss b/app/javascript/flavours/glitch/styles/mastodon/admin.scss
index 52c304c72a..658c0c85da 100644
--- a/app/javascript/flavours/glitch/styles/mastodon/admin.scss
+++ b/app/javascript/flavours/glitch/styles/mastodon/admin.scss
@@ -410,7 +410,7 @@ $content-width: 840px;
display: flex;
}
- & > ul {
+ & > nav > ul {
display: none;
&.visible {
diff --git a/app/javascript/flavours/glitch/styles/mastodon/components.scss b/app/javascript/flavours/glitch/styles/mastodon/components.scss
index 11e2203d34..b89fc2970d 100644
--- a/app/javascript/flavours/glitch/styles/mastodon/components.scss
+++ b/app/javascript/flavours/glitch/styles/mastodon/components.scss
@@ -1716,7 +1716,6 @@ body > [data-popper-placement] {
.status__quote-cancel {
align-self: self-start;
- order: 5;
}
.status__info {
@@ -11125,6 +11124,7 @@ noscript {
-webkit-box-orient: vertical;
max-height: 2 * 20px;
overflow: hidden;
+ overflow-wrap: anywhere;
p {
margin-bottom: 0;
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index d5f8ff1ea6..b2fbc6e25f 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -329,7 +329,7 @@ export function uploadCompose(files) {
dispatch(showAlert({ message: messages.uploadQuote }));
return;
}
- const uploadLimit = getState().getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']);
+ const uploadLimit = getState().getIn(['server', 'server', 'item', 'configuration', 'statuses', 'max_media_attachments']);
const media = getState().getIn(['compose', 'media_attachments']);
const pending = getState().getIn(['compose', 'pending_media_attachments']);
const progress = new Array(files.length).fill(0);
diff --git a/app/javascript/mastodon/actions/importer/emoji.ts b/app/javascript/mastodon/actions/importer/emoji.ts
index 9e06c88f66..e9356ab621 100644
--- a/app/javascript/mastodon/actions/importer/emoji.ts
+++ b/app/javascript/mastodon/actions/importer/emoji.ts
@@ -1,5 +1,8 @@
import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji';
import { loadCustomEmoji } from '@/mastodon/features/emoji';
+import { emojiLogger } from '@/mastodon/features/emoji/utils';
+
+const log = emojiLogger('actions');
export async function importCustomEmoji(emojis: ApiCustomEmojiJSON[]) {
if (emojis.length === 0) {
@@ -18,5 +21,11 @@ export async function importCustomEmoji(emojis: ApiCustomEmojiJSON[]) {
if (existingEmojis.length < emojis.length) {
await clearCache('custom');
await loadCustomEmoji();
+
+ const { reloadCustomEmojis } =
+ await import('@/mastodon/features/emoji/picker');
+ await reloadCustomEmojis();
+
+ log('Custom emojis updated, reloaded cache and picker data.');
}
}
diff --git a/app/javascript/mastodon/actions/server.ts b/app/javascript/mastodon/actions/server.ts
index 4e4795c123..b301919b23 100644
--- a/app/javascript/mastodon/actions/server.ts
+++ b/app/javascript/mastodon/actions/server.ts
@@ -16,19 +16,33 @@ export const fetchServer = createDataLoadingThunk(
dispatch(importFetchedAccount(instance.contact.account));
}
},
+ {
+ condition: (_, { getState }) => !getState().server.server.isLoading,
+ },
);
export const fetchExtendedDescription = createDataLoadingThunk(
'server/extended_description',
() => apiGetExtendedDescription(),
+ {
+ condition: (_, { getState }) =>
+ !getState().server.extendedDescription.isLoading,
+ },
);
export const fetchServerTranslationLanguages = createDataLoadingThunk(
'server/translation_languages',
() => apiGetTranslationLanguages(),
+ {
+ condition: (_, { getState }) =>
+ !getState().server.translationLanguages.isLoading,
+ },
);
export const fetchDomainBlocks = createDataLoadingThunk(
'server/domain_blocks',
() => apiGetDomainBlocks(),
+ {
+ condition: (_, { getState }) => !getState().server.domainBlocks.isLoading,
+ },
);
diff --git a/app/javascript/mastodon/components/account_header/styles.module.scss b/app/javascript/mastodon/components/account_header/styles.module.scss
index 2daf386734..ab751ce170 100644
--- a/app/javascript/mastodon/components/account_header/styles.module.scss
+++ b/app/javascript/mastodon/components/account_header/styles.module.scss
@@ -480,6 +480,7 @@ $button-fallback-breakpoint: $button-breakpoint + 55px;
justify-content: center;
align-items: flex-start;
margin: 16px 0;
+ padding: 16px;
}
.bannerBaseCentered {
diff --git a/app/javascript/mastodon/components/hover_card_controller.tsx b/app/javascript/mastodon/components/hover_card_controller.tsx
index a0c704a4e7..dd7ff3b3e7 100644
--- a/app/javascript/mastodon/components/hover_card_controller.tsx
+++ b/app/javascript/mastodon/components/hover_card_controller.tsx
@@ -144,11 +144,16 @@ export const HoverCardController: React.FC = () => {
setScrollTimeout(handleScrollEnd, 100);
};
- const handleMouseMove = () => {
+ const handleMouseMove = (e: MouseEvent) => {
if (isUsingTouch) {
isUsingTouch = false;
}
+ const hasMoved = Math.max(e.movementX, e.movementY) > 0;
+ if (!hasMoved) {
+ return;
+ }
+
delayEnterTimeout(enterDelay);
cancelMoveTimeout();
diff --git a/app/javascript/mastodon/components/scrollable_list/index.jsx b/app/javascript/mastodon/components/scrollable_list/index.jsx
index a80fe5581a..e44a827a2f 100644
--- a/app/javascript/mastodon/components/scrollable_list/index.jsx
+++ b/app/javascript/mastodon/components/scrollable_list/index.jsx
@@ -285,7 +285,7 @@ class ScrollableList extends PureComponent {
if (this.props.bindToDocument) {
document.removeEventListener('scroll', this.handleScroll);
document.removeEventListener('wheel', this.handleWheel, listenerOptions);
- } else {
+ } else if (this.node) {
this.node.removeEventListener('scroll', this.handleScroll);
this.node.removeEventListener('wheel', this.handleWheel, listenerOptions);
}
diff --git a/app/javascript/mastodon/components/status/header.tsx b/app/javascript/mastodon/components/status/header.tsx
index 65790bb493..1ce5c4a36c 100644
--- a/app/javascript/mastodon/components/status/header.tsx
+++ b/app/javascript/mastodon/components/status/header.tsx
@@ -20,7 +20,8 @@ export interface StatusHeaderProps {
status: Status;
account?: Account;
avatarSize?: number;
- children?: ReactNode;
+ contentBeforeDate?: ReactNode;
+ contentAfterDate?: ReactNode;
wrapperProps?: HTMLAttributes;
displayNameProps?: DisplayNameProps;
onHeaderClick?: MouseEventHandler;
@@ -33,10 +34,11 @@ export type StatusHeaderRenderFn = (args: StatusHeaderProps) => ReactNode;
export const StatusHeader: FC = ({
status,
account,
- children,
className,
avatarSize = 48,
wrapperProps,
+ contentBeforeDate,
+ contentAfterDate,
onHeaderClick,
}) => {
const statusAccount = status.get('account') as Account | undefined;
@@ -51,6 +53,14 @@ export const StatusHeader: FC = ({
className={classNames('status__info', className)}
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
>
+
+
+ {contentBeforeDate}
+
= ({
{editedAt && }
-
-
- {children}
+ {contentAfterDate}
);
};
diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx
index 4d9ae4fa3f..7e5cccf198 100644
--- a/app/javascript/mastodon/components/status_content.jsx
+++ b/app/javascript/mastodon/components/status_content.jsx
@@ -69,7 +69,7 @@ class TranslateButton extends PureComponent {
}
const mapStateToProps = state => ({
- languages: state.server.translationLanguages.items,
+ languages: state.server.translationLanguages.item,
});
class StatusContent extends PureComponent {
@@ -187,7 +187,7 @@ class StatusContent extends PureComponent {
const renderReadMore = this.props.onClick && status.get('collapsed');
const contentLocale = intl.locale.replace(/[_-].*/, '');
- const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
+ const targetLanguages = this.props.languages?.[status.get('language') || 'und'];
const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
const content = statusContent ?? getStatusContent(status);
diff --git a/app/javascript/mastodon/components/status_quoted.tsx b/app/javascript/mastodon/components/status_quoted.tsx
index 5c9804fb40..b269792a5e 100644
--- a/app/javascript/mastodon/components/status_quoted.tsx
+++ b/app/javascript/mastodon/components/status_quoted.tsx
@@ -225,17 +225,20 @@ export const QuotedStatus: React.FC
= ({
const intl = useIntl();
const headerRenderFn: StatusHeaderRenderFn = useCallback(
(props) => (
-
- {onQuoteCancel && (
-
- )}
-
+
+ )
+ }
+ />
),
[intl, onQuoteCancel],
);
diff --git a/app/javascript/mastodon/features/account_timeline/components/pinned_statuses.tsx b/app/javascript/mastodon/features/account_timeline/components/pinned_statuses.tsx
index 11de336002..c751af83ca 100644
--- a/app/javascript/mastodon/features/account_timeline/components/pinned_statuses.tsx
+++ b/app/javascript/mastodon/features/account_timeline/components/pinned_statuses.tsx
@@ -22,18 +22,22 @@ export const renderPinnedStatusHeader: StatusHeaderRenderFn = ({
return ;
}
return (
-
- }
- label={
-
- }
- />
-
+ }
+ label={
+
+ }
+ />
+ }
+ />
);
};
diff --git a/app/javascript/mastodon/features/account_timeline/styles.module.scss b/app/javascript/mastodon/features/account_timeline/styles.module.scss
index cd5cef3f29..9773861d22 100644
--- a/app/javascript/mastodon/features/account_timeline/styles.module.scss
+++ b/app/javascript/mastodon/features/account_timeline/styles.module.scss
@@ -126,4 +126,7 @@
.pinnedBadge {
justify-self: end;
+
+ // Allow "click to open post" event to pass through
+ pointer-events: none;
}
diff --git a/app/javascript/mastodon/features/collections/components/collection_list_item.tsx b/app/javascript/mastodon/features/collections/components/collection_list_item.tsx
index 8169544cb8..c1b32cac27 100644
--- a/app/javascript/mastodon/features/collections/components/collection_list_item.tsx
+++ b/app/javascript/mastodon/features/collections/components/collection_list_item.tsx
@@ -27,14 +27,14 @@ export const CollectionListItem: React.FC = ({
...otherProps
}) => {
const uniqueId = useId();
- const linkId = `${uniqueId}-link`;
- const infoId = `${uniqueId}-info`;
+ const titleId = `${uniqueId}-title`;
+ const subtitleId = `${uniqueId}-info`;
return (
@@ -52,6 +52,8 @@ export const CollectionListItem: React.FC = ({
className={classes.menuButton}
/>
}
+ titleId={titleId}
+ subtitleId={subtitleId}
{...otherProps}
/>
diff --git a/app/javascript/mastodon/features/collections/components/collection_lockup.tsx b/app/javascript/mastodon/features/collections/components/collection_lockup.tsx
index e16d49e792..5250ea5504 100644
--- a/app/javascript/mastodon/features/collections/components/collection_lockup.tsx
+++ b/app/javascript/mastodon/features/collections/components/collection_lockup.tsx
@@ -48,6 +48,8 @@ export interface CollectionLockupProps {
sideContent?: React.ReactNode;
className?: string;
headingLevel?: 'h2' | 'h3' | 'h4';
+ titleId?: string;
+ subtitleId?: string;
}
export const CollectionLockup: React.FC = ({
@@ -56,6 +58,8 @@ export const CollectionLockup: React.FC = ({
withTimestamp,
sideContent,
headingLevel = 'h3',
+ titleId,
+ subtitleId,
className,
}) => {
const { id, name } = collection;
@@ -74,6 +78,7 @@ export const CollectionLockup: React.FC = ({
= ({
withTimestamp={withTimestamp}
/>
}
+ subtitleId={subtitleId}
>
{name}
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx
index a646a7efae..2cc1f7915a 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.jsx
+++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx
@@ -248,7 +248,15 @@ class ComposeForm extends ImmutablePureComponent {
const { intl, onPaste, onDrop, autoFocus, withoutNavigation, maxChars, isSubmitting } = this.props;
return (
-