diff --git a/src/App.tsx b/src/App.tsx index 4ba4f96..6413bbf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,87 +1,13 @@ -import { FC, useEffect, useState } from 'react' -import { Container, Dimmer, Loader } from 'semantic-ui-react' -import { Settings, Ticker } from './lib/types' -import { getInit } from './lib/api' -import ErrorView from './views/ErrorView' -import ActiveView from './views/ActiveView' -import InactiveView from './views/InactiveView' +import { FC } from 'react' +import { TickerProvider } from './components/useTicker' +import Ticker from './Ticker' const App: FC = () => { - const [ticker, setTicker] = useState(null) - const [settings, setSettings] = useState() - const [isLoading, setIsLoading] = useState(true) - const [isOffline, setIsOffline] = useState(false) - const [gotError, setGotError] = useState(false) - - const fetchInit = () => { - getInit() - .then(response => { - if (response.data.settings) { - setSettings(response.data.settings) - } - - if (response.data.ticker?.active) { - setTicker(response.data.ticker) - } - - setIsLoading(false) - }) - .catch(error => { - if (error instanceof TypeError) { - setIsOffline(true) - } else { - setGotError(true) - } - setIsLoading(false) - }) - } - - useEffect(() => { - fetchInit() - // This should only be executed once on load (~ componentDidMount) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - useEffect(() => { - if (ticker?.title) { - document.title = ticker.title - } - }, [ticker]) - - if (isLoading) { - return ( - - - - - - ) - } - - if (gotError) { - return ( - - ) - } - - if (isOffline) { - return - } - - if (ticker?.active) { - return ( - - ) - } - - if (ticker === null && settings?.inactive_settings !== undefined) { - return - } - - return
...
+ return ( + + + + ) } export default App diff --git a/src/App.test.tsx b/src/Ticker.test.tsx similarity index 87% rename from src/App.test.tsx rename to src/Ticker.test.tsx index f729e5c..d670525 100644 --- a/src/App.test.tsx +++ b/src/Ticker.test.tsx @@ -1,9 +1,10 @@ -import App from './App' +import Ticker from './Ticker' import { render, screen } from '@testing-library/react' import * as api from './lib/api' -import { Settings, Ticker } from './lib/types' +import { Settings, Ticker as TickerType } from './lib/types' +import { TickerProvider } from './components/useTicker' -describe('App', function () { +describe('Ticker', function () { const initSettings = { refresh_interval: 1000, inactive_settings: { @@ -30,11 +31,19 @@ describe('App', function () { twitter: 'systemli', facebook: 'betternot', }, - } as Ticker + } as TickerType + + const renderTicker = () => { + return render( + + + + ) + } test('renders OfflineView', async function () { jest.spyOn(api, 'getInit').mockRejectedValue(new TypeError()) - render() + renderTicker() expect(screen.getByText('Loading')).toBeInTheDocument() @@ -51,7 +60,7 @@ describe('App', function () { 'The server responses with an error: Internal Server Error (500)' ) ) - render() + renderTicker() expect(screen.getByText('Loading')).toBeInTheDocument() @@ -69,7 +78,7 @@ describe('App', function () { ticker: null, }, }) - render() + renderTicker() expect(screen.getByText('Loading')).toBeInTheDocument() @@ -96,7 +105,7 @@ describe('App', function () { window.IntersectionObserver = jest .fn() .mockImplementation(intersectionObserverMock) - render() + renderTicker() expect(screen.getByText('Loading')).toBeInTheDocument() diff --git a/src/Ticker.tsx b/src/Ticker.tsx new file mode 100644 index 0000000..b3c0ef0 --- /dev/null +++ b/src/Ticker.tsx @@ -0,0 +1,48 @@ +import { FC, useEffect } from 'react' +import { Container, Dimmer, Loader } from 'semantic-ui-react' +import ErrorView from './views/ErrorView' +import ActiveView from './views/ActiveView' +import InactiveView from './views/InactiveView' +import useTicker from './components/useTicker' + +const Ticker: FC = () => { + const { ticker, settings, isLoading, isOffline, hasError } = useTicker() + + useEffect(() => { + if (ticker?.title) { + document.title = ticker.title + } + }, [ticker]) + + if (isLoading) { + return ( + + + + + + ) + } + + if (hasError) { + return ( + + ) + } + + if (isOffline) { + return + } + + if (ticker?.active) { + return + } + + if (ticker === null && settings?.inactive_settings !== undefined) { + return + } + + return
...
+} + +export default Ticker diff --git a/src/components/About.tsx b/src/components/About.tsx index 59ab191..1e5a6d3 100644 --- a/src/components/About.tsx +++ b/src/components/About.tsx @@ -3,50 +3,56 @@ import ReactMarkdown from 'react-markdown' import { Button, Card, Icon, List, Modal } from 'semantic-ui-react' import Credits from './Credits' import DescriptionItem from './DescriptionItem' -import { DescriptionTypes, Ticker } from '../lib/types' +import { DescriptionTypes } from '../lib/types' import { getAtomFeedUrl, getRssFeedUrl } from '../lib/api' +import useTicker from './useTicker' interface Props { - ticker: Ticker isModal?: boolean } -const About: FC = props => { +const About: FC = ({ isModal }) => { + const { ticker } = useTicker() + + if (!ticker) { + return null + } + const renderDescriptionList = () => ( - {props.ticker.information.author && ( + {ticker.information.author && ( )} - {props.ticker.information.email && ( + {ticker.information.email && ( )} - {props.ticker.information.url && ( + {ticker.information.url && ( )} - {props.ticker.information.twitter && ( + {ticker.information.twitter && ( )} - {props.ticker.information.facebook && ( + {ticker.information.facebook && ( )} - {props.ticker.information.telegram && ( + {ticker.information.telegram && ( )} @@ -60,7 +66,7 @@ const About: FC = props => { ) - if (props.isModal) { + if (isModal) { return ( = props => { > About - {props.ticker.description} + {ticker.description || ''} {renderDescriptionList()} @@ -90,7 +96,7 @@ const About: FC = props => { About - {props.ticker.description} + {ticker.description || ''} {renderDescriptionList()} diff --git a/src/components/DynamicMetaTags.tsx b/src/components/DynamicMetaTags.tsx index 41acc1b..620b175 100644 --- a/src/components/DynamicMetaTags.tsx +++ b/src/components/DynamicMetaTags.tsx @@ -1,12 +1,14 @@ import { FC } from 'react' import { Helmet } from 'react-helmet' -import { Ticker } from '../lib/types' +import useTicker from './useTicker' -interface Props { - ticker: Ticker -} +const DynamicMetaTags: FC = () => { + const { ticker } = useTicker() + + if (!ticker) { + return null + } -const DynamicMetaTags: FC = ({ ticker }) => { return ( diff --git a/src/components/Map.tsx b/src/components/Map.tsx index 606a28a..368389b 100644 --- a/src/components/Map.tsx +++ b/src/components/Map.tsx @@ -56,7 +56,7 @@ const Map: FC = props => { if ( features.length === 1 && - // FIXME: Type is currently not defined by DefinitelyTyped? + // Type is currently not defined by DefinitelyTyped // @ts-ignore features[0].feature.geometry.type === 'Point' ) { diff --git a/src/components/MessageList.test.tsx b/src/components/MessageList.test.tsx index e50063c..db35c58 100644 --- a/src/components/MessageList.test.tsx +++ b/src/components/MessageList.test.tsx @@ -14,7 +14,7 @@ describe('MessageList', function () { .fn() .mockImplementation(intersectionObserverMock) - render() + render() expect(screen.getByText('Loading messages')).toBeInTheDocument() expect( diff --git a/src/components/MessageList.tsx b/src/components/MessageList.tsx index 2c05268..f7af77b 100644 --- a/src/components/MessageList.tsx +++ b/src/components/MessageList.tsx @@ -3,16 +3,15 @@ import { Dimmer, Header, Icon, Loader, Segment } from 'semantic-ui-react' import { Message as MessageType } from '../lib/types' import Message from './Message' import { getTimeline } from '../lib/api' +import useTicker from './useTicker' -interface Props { - refreshInterval: number -} - -const MessageList: FC = props => { +const MessageList: FC = () => { const [isLoading, setIsLoading] = useState(true) const [messages, setMessages] = useState([]) const [lastMessageReceived, setLastMessageReceived] = useState(false) + const { settings } = useTicker() + const loadMoreSpinnerRef = useRef(null) const fetchMessages = useCallback(() => { @@ -98,10 +97,13 @@ const MessageList: FC = props => { // periodically fetch new messages useEffect(() => { - const interval = setInterval(() => fetchMessages(), props.refreshInterval) + const interval = setInterval( + () => fetchMessages(), + settings?.refresh_interval || 60000 + ) return () => clearInterval(interval) - }, [fetchMessages, messages, props.refreshInterval]) + }, [fetchMessages, messages, settings?.refresh_interval]) const renderPlaceholder = () => ( diff --git a/src/components/useTicker.tsx b/src/components/useTicker.tsx new file mode 100644 index 0000000..fe3d418 --- /dev/null +++ b/src/components/useTicker.tsx @@ -0,0 +1,80 @@ +import React, { + createContext, + ReactNode, + useContext, + useEffect, + useMemo, +} from 'react' +import { getInit } from '../lib/api' +import { Settings, Ticker } from '../lib/types' +import { useState } from 'react' + +interface TickerContext { + ticker: Ticker | null + settings?: Settings + isLoading: boolean + isOffline: boolean + hasError: boolean +} + +const TickerContext = createContext({} as TickerContext) + +export function TickerProvider({ + children, +}: { + children: ReactNode +}): JSX.Element { + const [ticker, setTicker] = useState(null) + const [settings, setSettings] = useState() + const [isLoading, setIsLoading] = useState(true) + const [isOffline, setIsOffline] = useState(false) + const [hasError, setHasError] = useState(false) + + const fetchInit = () => { + getInit() + .then(response => { + if (response.data.settings) { + setSettings(response.data.settings) + } + + if (response.data.ticker?.active) { + setTicker(response.data.ticker) + } + + setIsLoading(false) + }) + .catch(error => { + if (error instanceof TypeError) { + setIsOffline(true) + } else { + setHasError(true) + } + setIsLoading(false) + }) + } + + useEffect(() => { + fetchInit() + }, []) + + const memoedValue = useMemo( + () => ({ + ticker, + settings, + isLoading, + isOffline, + hasError, + }), + [ticker, settings, isLoading, isOffline, hasError] + ) + + return ( + + {children} + + ) +} + +export default function useTicker() { + return useContext(TickerContext) +} diff --git a/src/views/ActiveView.tsx b/src/views/ActiveView.tsx index 40f4876..85df211 100644 --- a/src/views/ActiveView.tsx +++ b/src/views/ActiveView.tsx @@ -2,12 +2,12 @@ import { FC, useCallback, useState } from 'react' import { Container, Grid, Header, Sticky } from 'semantic-ui-react' import styled from 'styled-components' import { spacing } from '../lib/theme' -import { Ticker } from '../lib/types' import { isMobile } from '../lib/helper' import About from '../components/About' import ReloadInfo from '../components/ReloadInfo' import MessageList from '../components/MessageList' import DynamicMetaTags from '../components/DynamicMetaTags' +import useTicker from '../components/useTicker' const Wrapper = styled(Container)` padding: ${spacing.normal} 0; @@ -17,15 +17,11 @@ const HeaderWrapper = styled(Header)` margin: 0 0 ${spacing.normal} !important; ` -interface Props { - ticker: Ticker - refreshInterval: number -} - type StickyContext = Document | Window | HTMLElement | React.Ref -const ActiveView: FC = ({ ticker, refreshInterval }) => { +const ActiveView: FC = () => { const [stickyContext, setStickyContext] = useState() + const { ticker } = useTicker() const headline = ticker === null || ticker.title == undefined ? 'Ticker' : ticker.title @@ -37,30 +33,30 @@ const ActiveView: FC = ({ ticker, refreshInterval }) => { if (isMobile()) { return ( - - + + {headline && } - + ) } return ( - + {headline && }
- +
- +