diff --git a/app/javascript/flavours/glitch/actions/server.ts b/app/javascript/flavours/glitch/actions/server.ts index 825ca22c6c..4b5103de72 100644 --- a/app/javascript/flavours/glitch/actions/server.ts +++ b/app/javascript/flavours/glitch/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/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 = () => ( +
+
+ {domain} +
+ +
+ + + +
+
+); 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/ui/components/columns_area.tsx b/app/javascript/flavours/glitch/features/ui/components/columns_area.tsx index 547e62a838..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 (
@@ -112,6 +133,7 @@ export const ColumnsArea = forwardRef<
+
{children}
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 e2953d879e..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. @@ -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 && }