Add new overview landing page setting (#39074)
This commit is contained in:
parent
f6d1795da5
commit
07d099cbf7
@ -16,19 +16,33 @@ export const fetchServer = createDataLoadingThunk(
|
|||||||
dispatch(importFetchedAccount(instance.contact.account));
|
dispatch(importFetchedAccount(instance.contact.account));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
condition: (_, { getState }) => !getState().server.server.isLoading,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const fetchExtendedDescription = createDataLoadingThunk(
|
export const fetchExtendedDescription = createDataLoadingThunk(
|
||||||
'server/extended_description',
|
'server/extended_description',
|
||||||
() => apiGetExtendedDescription(),
|
() => apiGetExtendedDescription(),
|
||||||
|
{
|
||||||
|
condition: (_, { getState }) =>
|
||||||
|
!getState().server.extendedDescription.isLoading,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const fetchServerTranslationLanguages = createDataLoadingThunk(
|
export const fetchServerTranslationLanguages = createDataLoadingThunk(
|
||||||
'server/translation_languages',
|
'server/translation_languages',
|
||||||
() => apiGetTranslationLanguages(),
|
() => apiGetTranslationLanguages(),
|
||||||
|
{
|
||||||
|
condition: (_, { getState }) =>
|
||||||
|
!getState().server.translationLanguages.isLoading,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const fetchDomainBlocks = createDataLoadingThunk(
|
export const fetchDomainBlocks = createDataLoadingThunk(
|
||||||
'server/domain_blocks',
|
'server/domain_blocks',
|
||||||
() => apiGetDomainBlocks(),
|
() => apiGetDomainBlocks(),
|
||||||
|
{
|
||||||
|
condition: (_, { getState }) => !getState().server.domainBlocks.isLoading,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
75
app/javascript/mastodon/features/custom_homepage/about.tsx
Normal file
75
app/javascript/mastodon/features/custom_homepage/about.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { fetchExtendedDescription } from 'mastodon/actions/server';
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
|
import { Skeleton } from 'mastodon/components/skeleton';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
|
const Placeholder = () => (
|
||||||
|
<div className={classes.placeholder}>
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<div className={classes.block}>
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage
|
||||||
|
id='custom_homepage.administered_by'
|
||||||
|
defaultMessage='Administered by'
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
<Account id={accountId} size={36} minimal />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classes.block}>
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage
|
||||||
|
id='custom_homepage.about_this_server'
|
||||||
|
defaultMessage='About this server'
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
{isLoading ? (
|
||||||
|
<Placeholder />
|
||||||
|
) : hasContent ? (
|
||||||
|
<div
|
||||||
|
className='prose'
|
||||||
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className='prose'>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='about.not_available'
|
||||||
|
defaultMessage='This information has not been made available on this server.'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { fetchServer } from 'mastodon/actions/server';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/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 (
|
||||||
|
<footer className={classes.minimalFooter}>
|
||||||
|
<div className={classes.contact}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='custom_homepage.contact'
|
||||||
|
defaultMessage='Contact:'
|
||||||
|
/>
|
||||||
|
<a href={`mailto:${email}`}>{email}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link to='/privacy-policy' rel='privacy-policy'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='footer.privacy_policy'
|
||||||
|
defaultMessage='Privacy policy'
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { domain, sso_redirect } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
import classes from '../styles.module.scss';
|
||||||
|
|
||||||
|
export const Header = () => (
|
||||||
|
<div className={classes.minimalHeader}>
|
||||||
|
<div className={classes.leftSide}>
|
||||||
|
<Link to='/overview'>{domain}</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classes.rightSide}>
|
||||||
|
<a
|
||||||
|
href={sso_redirect ?? '/auth/sign_in'}
|
||||||
|
data-method={sso_redirect ? 'post' : undefined}
|
||||||
|
className='button button-secondary'
|
||||||
|
>
|
||||||
|
<FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
71
app/javascript/mastodon/features/custom_homepage/index.tsx
Normal file
71
app/javascript/mastodon/features/custom_homepage/index.tsx
Normal file
@ -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 'mastodon/actions/server';
|
||||||
|
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
|
||||||
|
import { TabLink, TabList } from 'mastodon/components/tab_list';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'mastodon/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 (
|
||||||
|
<div className={classes.page}>
|
||||||
|
<ServerHeroImage
|
||||||
|
alt={server.item?.thumbnail.description ?? ''}
|
||||||
|
blurhash={server.item?.thumbnail.blurhash ?? ''}
|
||||||
|
src={server.item?.thumbnail.url ?? ''}
|
||||||
|
srcSet={Object.keys(server.item?.thumbnail.versions ?? {})
|
||||||
|
.map(
|
||||||
|
(key) =>
|
||||||
|
`${server.item?.thumbnail.versions?.[key]} ${key.replace('@', '')}`,
|
||||||
|
)
|
||||||
|
.join(', ')}
|
||||||
|
className={classes.header}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={classes.topSection}>
|
||||||
|
<h1>{server.item?.domain}</h1>
|
||||||
|
<p>{server.item?.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TabList>
|
||||||
|
<TabLink to={path} exact>
|
||||||
|
<FormattedMessage
|
||||||
|
id='custom_homepage.latest_activity'
|
||||||
|
defaultMessage='Latest activity'
|
||||||
|
/>
|
||||||
|
</TabLink>
|
||||||
|
|
||||||
|
<TabLink to={`${path}/about`} exact>
|
||||||
|
<FormattedMessage id='custom_homepage.about' defaultMessage='About' />
|
||||||
|
</TabLink>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<Switch>
|
||||||
|
<Route path={path} exact component={LatestActivity} />
|
||||||
|
<Route path={`${path}/about`} exact component={About} />
|
||||||
|
</Switch>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{server.item?.domain}</title>
|
||||||
|
<meta name='robots' content='all' />
|
||||||
|
</Helmet>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { expandCommunityTimeline } from 'mastodon/actions/timelines';
|
||||||
|
import { Callout } from 'mastodon/components/callout';
|
||||||
|
import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
|
||||||
|
import { useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
|
export const LatestActivity = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void dispatch(expandCommunityTimeline());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusListContainer
|
||||||
|
prepend={
|
||||||
|
<Callout className={classes.banner}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='custom_homepage.these_are_the_latest_posts'
|
||||||
|
defaultMessage='These are the latest 40 posts from accounts on this server.'
|
||||||
|
/>
|
||||||
|
</Callout>
|
||||||
|
}
|
||||||
|
scrollKey='custom_homepage'
|
||||||
|
timelineId='community'
|
||||||
|
maxItems={40}
|
||||||
|
bindToDocument
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -12,6 +12,8 @@ import classNames from 'classnames';
|
|||||||
import type { List, Record } from 'immutable';
|
import type { List, Record } from 'immutable';
|
||||||
|
|
||||||
import { useAppSelector } from '@/mastodon/store';
|
import { useAppSelector } from '@/mastodon/store';
|
||||||
|
import { Footer } from 'mastodon/features/custom_homepage/components/footer';
|
||||||
|
import { Header } from 'mastodon/features/custom_homepage/components/header';
|
||||||
import { CollapsibleNavigationPanel } from 'mastodon/features/navigation_panel';
|
import { CollapsibleNavigationPanel } from 'mastodon/features/navigation_panel';
|
||||||
|
|
||||||
import { useBreakpoint } from '../hooks/useBreakpoint';
|
import { useBreakpoint } from '../hooks/useBreakpoint';
|
||||||
@ -85,9 +87,10 @@ export const ColumnsArea = forwardRef<
|
|||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
{
|
{
|
||||||
singleColumn?: boolean;
|
singleColumn?: boolean;
|
||||||
|
minimalShell?: boolean;
|
||||||
children: React.ReactElement | React.ReactElement[];
|
children: React.ReactElement | React.ReactElement[];
|
||||||
}
|
}
|
||||||
>(({ children, singleColumn }, ref) => {
|
>(({ children, minimalShell, singleColumn }, ref) => {
|
||||||
const renderComposePanel = !useBreakpoint('full');
|
const renderComposePanel = !useBreakpoint('full');
|
||||||
const columns = useAppSelector((state) =>
|
const columns = useAppSelector((state) =>
|
||||||
(state.settings as Record<{ columns: List<Record<Column>> }>).get(
|
(state.settings as Record<{ columns: List<Record<Column>> }>).get(
|
||||||
@ -98,6 +101,24 @@ export const ColumnsArea = forwardRef<
|
|||||||
(state) => !state.modal.get('stack').isEmpty(),
|
(state) => !state.modal.get('stack').isEmpty(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (minimalShell) {
|
||||||
|
return (
|
||||||
|
<div className='columns-area__panels'>
|
||||||
|
<div className='columns-area__panels__main'>
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<div className='tabs-bar__wrapper'>
|
||||||
|
<TabsBarPortal />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (singleColumn) {
|
if (singleColumn) {
|
||||||
return (
|
return (
|
||||||
<div className='columns-area__panels'>
|
<div className='columns-area__panels'>
|
||||||
@ -112,6 +133,7 @@ export const ColumnsArea = forwardRef<
|
|||||||
<div className='tabs-bar__wrapper'>
|
<div className='tabs-bar__wrapper'>
|
||||||
<TabsBarPortal />
|
<TabsBarPortal />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,15 @@ import { me } from '@/mastodon/initial_state';
|
|||||||
|
|
||||||
const makeGetStatusIds = (pending = false) => createSelector([
|
const makeGetStatusIds = (pending = false) => createSelector([
|
||||||
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
(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'),
|
(state) => state.get('statuses'),
|
||||||
], (columnSettings, statusIds, statuses) => {
|
], (columnSettings, statusIds, statuses) => {
|
||||||
return statusIds.filter(id => {
|
return statusIds.filter(id => {
|
||||||
@ -41,8 +49,15 @@ const makeMapStateToProps = () => {
|
|||||||
const getStatusIds = makeGetStatusIds();
|
const getStatusIds = makeGetStatusIds();
|
||||||
const getPendingStatusIds = makeGetStatusIds(true);
|
const getPendingStatusIds = makeGetStatusIds(true);
|
||||||
|
|
||||||
const mapStateToProps = (state, { timelineId, initialLoadingState = true }) => ({
|
/**
|
||||||
statusIds: getStatusIds(state, { type: timelineId }),
|
* @param {import('mastodon/store').RootState} state
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {string} props.timelineId
|
||||||
|
* @param {boolean} [props.initialLoadingState]
|
||||||
|
* @param {number} [props.maxItems]
|
||||||
|
*/
|
||||||
|
const mapStateToProps = (state, { timelineId, initialLoadingState = true, maxItems }) => ({
|
||||||
|
statusIds: getStatusIds(state, { type: timelineId, maxItems }),
|
||||||
lastId: state.getIn(['timelines', timelineId, 'items'])?.last(),
|
lastId: state.getIn(['timelines', timelineId, 'items'])?.last(),
|
||||||
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], initialLoadingState),
|
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], initialLoadingState),
|
||||||
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
|
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../act
|
|||||||
import { clearHeight } from '../../actions/height_cache';
|
import { clearHeight } from '../../actions/height_cache';
|
||||||
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
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 BundleColumnError from './components/bundle_column_error';
|
||||||
import { NavigationBar } from './components/navigation_bar';
|
import { NavigationBar } from './components/navigation_bar';
|
||||||
@ -88,6 +88,7 @@ import {
|
|||||||
import { ColumnsContextProvider } from './util/columns_context';
|
import { ColumnsContextProvider } from './util/columns_context';
|
||||||
import { focusColumn, getFocusedItemIndex, focusItemSibling, focusFirstItem } from './util/focusUtils';
|
import { focusColumn, getFocusedItemIndex, focusItemSibling, focusFirstItem } from './util/focusUtils';
|
||||||
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
||||||
|
import { CustomHomepage } from 'mastodon/features/custom_homepage';
|
||||||
|
|
||||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||||
// Without this it ends up in ~8 very commonly used bundles.
|
// Without this it ends up in ~8 very commonly used bundles.
|
||||||
@ -177,13 +178,15 @@ class SwitchingColumnsArea extends PureComponent {
|
|||||||
rootRedirect = '/explore';
|
rootRedirect = '/explore';
|
||||||
} else if (localLiveFeedAccess === 'public' && landingPage === 'local_feed') {
|
} else if (localLiveFeedAccess === 'public' && landingPage === 'local_feed') {
|
||||||
rootRedirect = '/public/local';
|
rootRedirect = '/public/local';
|
||||||
|
} else if (landingPage === 'overview') {
|
||||||
|
rootRedirect = '/overview';
|
||||||
} else {
|
} else {
|
||||||
rootRedirect = '/about';
|
rootRedirect = '/about';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColumnsContextProvider multiColumn={!singleColumn}>
|
<ColumnsContextProvider multiColumn={!singleColumn}>
|
||||||
<ColumnsArea ref={this.setRef} singleColumn={singleColumn}>
|
<ColumnsArea ref={this.setRef} singleColumn={singleColumn} domain={domain} minimalShell={!signedIn && landingPage === 'overview'}>
|
||||||
<WrappedSwitch>
|
<WrappedSwitch>
|
||||||
<Redirect from='/' to={{pathname: rootRedirect, state: this.props.location.state}} exact />
|
<Redirect from='/' to={{pathname: rootRedirect, state: this.props.location.state}} exact />
|
||||||
|
|
||||||
@ -262,6 +265,8 @@ class SwitchingColumnsArea extends PureComponent {
|
|||||||
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
|
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
|
||||||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||||
|
|
||||||
|
<Route path='/overview' component={CustomHomepage} />
|
||||||
<Route component={BundleColumnError} />
|
<Route component={BundleColumnError} />
|
||||||
</WrappedSwitch>
|
</WrappedSwitch>
|
||||||
</ColumnsArea>
|
</ColumnsArea>
|
||||||
@ -633,13 +638,18 @@ class UI extends PureComponent {
|
|||||||
cheat: this.handleDonate,
|
cheat: this.handleDonate,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const minimalShell = !this.props.identity.signedIn && landingPage === 'overview';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Hotkeys global handlers={handlers}>
|
<Hotkeys global handlers={handlers}>
|
||||||
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef}>
|
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef}>
|
||||||
<SkipLinks
|
{!minimalShell && (
|
||||||
multiColumn={layout === 'multi-column'}
|
<SkipLinks
|
||||||
onFocusGettingStartedColumn={this.handleHotkeyGoToStart}
|
multiColumn={layout === 'multi-column'}
|
||||||
/>
|
onFocusGettingStartedColumn={this.handleHotkeyGoToStart}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<SwitchingColumnsArea
|
<SwitchingColumnsArea
|
||||||
identity={this.props.identity}
|
identity={this.props.identity}
|
||||||
location={location}
|
location={location}
|
||||||
@ -650,7 +660,7 @@ class UI extends PureComponent {
|
|||||||
{children}
|
{children}
|
||||||
</SwitchingColumnsArea>
|
</SwitchingColumnsArea>
|
||||||
|
|
||||||
<NavigationBar />
|
{!minimalShell && <NavigationBar />}
|
||||||
{layout !== 'mobile' && <PictureInPicture />}
|
{layout !== 'mobile' && <PictureInPicture />}
|
||||||
<AlertsController />
|
<AlertsController />
|
||||||
{!disableHoverCards && <HoverCardController />}
|
{!disableHoverCards && <HoverCardController />}
|
||||||
|
|||||||
@ -584,6 +584,12 @@
|
|||||||
"copy_icon_button.copy_this_text": "Copy link to clipboard",
|
"copy_icon_button.copy_this_text": "Copy link to clipboard",
|
||||||
"copypaste.copied": "Copied",
|
"copypaste.copied": "Copied",
|
||||||
"copypaste.copy_to_clipboard": "Copy to clipboard",
|
"copypaste.copy_to_clipboard": "Copy to clipboard",
|
||||||
|
"custom_homepage.about": "About",
|
||||||
|
"custom_homepage.about_this_server": "About this server",
|
||||||
|
"custom_homepage.administered_by": "Administered by",
|
||||||
|
"custom_homepage.contact": "Contact:",
|
||||||
|
"custom_homepage.latest_activity": "Latest activity",
|
||||||
|
"custom_homepage.these_are_the_latest_posts": "These are the latest 40 posts from accounts on this server.",
|
||||||
"directory.federated": "From known fediverse",
|
"directory.federated": "From known fediverse",
|
||||||
"directory.local": "From {domain} only",
|
"directory.local": "From {domain} only",
|
||||||
"directory.new_arrivals": "New arrivals",
|
"directory.new_arrivals": "New arrivals",
|
||||||
|
|||||||
@ -94,7 +94,7 @@ class Form::AdminSettings
|
|||||||
REGISTRATION_MODES = %w(open approved none).freeze
|
REGISTRATION_MODES = %w(open approved none).freeze
|
||||||
FEED_ACCESS_MODES = %w(public authenticated disabled).freeze
|
FEED_ACCESS_MODES = %w(public authenticated disabled).freeze
|
||||||
ALTERNATE_FEED_ACCESS_MODES = %w(public authenticated).freeze
|
ALTERNATE_FEED_ACCESS_MODES = %w(public authenticated).freeze
|
||||||
LANDING_PAGE = %w(trends about local_feed).freeze
|
LANDING_PAGE = %w(trends overview local_feed about).freeze
|
||||||
|
|
||||||
attr_accessor(*KEYS)
|
attr_accessor(*KEYS)
|
||||||
|
|
||||||
|
|||||||
@ -75,10 +75,11 @@
|
|||||||
|
|
||||||
.fields-row
|
.fields-row
|
||||||
= f.input :landing_page,
|
= f.input :landing_page,
|
||||||
|
as: :radio_buttons,
|
||||||
collection: f.object.class::LANDING_PAGE,
|
collection: f.object.class::LANDING_PAGE,
|
||||||
include_blank: false,
|
include_blank: false,
|
||||||
label_method: ->(page) { I18n.t("admin.settings.landing_page.values.#{page}") },
|
label_method: ->(page) { safe_join([I18n.t("admin.settings.landing_page.values.#{page}"), content_tag(:span, I18n.t("admin.settings.landing_page.hints.#{page}_html"), class: 'hint')]) },
|
||||||
wrapper: :with_label
|
wrapper: :with_block_label
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('generic.save_changes'), type: :submit
|
= f.button :button, t('generic.save_changes'), type: :submit
|
||||||
|
|||||||
@ -956,10 +956,16 @@ en:
|
|||||||
disabled: Require specific user role
|
disabled: Require specific user role
|
||||||
public: Everyone
|
public: Everyone
|
||||||
landing_page:
|
landing_page:
|
||||||
|
hints:
|
||||||
|
about_html: A page with the description, contact information, rules and other information regarding this server.
|
||||||
|
local_feed_html: A live feed featuring most recent posts by users on this server.
|
||||||
|
overview_html: A page showcasing the description of your server alongside the most recent local posts by users on this server.
|
||||||
|
trends_html: A page featuring what's popular on this server right now.
|
||||||
values:
|
values:
|
||||||
about: About
|
about: About page
|
||||||
local_feed: Local feed
|
local_feed: Local live feed
|
||||||
trends: Trends
|
overview: Overview
|
||||||
|
trends: Trending
|
||||||
registrations:
|
registrations:
|
||||||
moderation_recommandation: Please make sure you have an adequate and reactive moderation team before you open registrations to everyone!
|
moderation_recommandation: Please make sure you have an adequate and reactive moderation team before you open registrations to everyone!
|
||||||
preamble: Control who can create an account on your server.
|
preamble: Control who can create an account on your server.
|
||||||
|
|||||||
@ -33,4 +33,6 @@
|
|||||||
/search
|
/search
|
||||||
/start/(*any)
|
/start/(*any)
|
||||||
/statuses/(*any)
|
/statuses/(*any)
|
||||||
|
/overview
|
||||||
|
/overview/about
|
||||||
).each { |path| get path, to: 'home#index' }
|
).each { |path| get path, to: 'home#index' }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user