Skip to content

Commit

Permalink
Merge pull request #264 from UnofficialCrusaderPatch/265-news-ui
Browse files Browse the repository at this point in the history
Add news feature
  • Loading branch information
gynt authored Oct 16, 2024
2 parents 5469051 + 4b4ff7d commit d05db34
Show file tree
Hide file tree
Showing 15 changed files with 449 additions and 1 deletion.
2 changes: 2 additions & 0 deletions resources/lang/sources/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ launch.ucp2.check.title: "UCP2 detected!"
loading: "Loading..."
locked.files: "Some game files are locked, is the game running? Close the game first. If the game is not running, make sure the GUI has security access to the files in the game folder."
messages: "{{count}} messages"
news.title: "News"
news.button: "News"
no: "No"
ok: "Ok"
old.folders: "Use one of the recently used folders:"
Expand Down
28 changes: 28 additions & 0 deletions src/components/news/news-item-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { forwardRef, Ref } from 'react';
import { newsID } from '../../function/news/state';
import { NewsElement } from '../../function/news/types';
import { NewsItem } from './news-item';

/**
* Wrapper for a NewsItem. The Wrapper has an id which is used
* for element.scrollIntoView() behavior.
* Scroll into view occurs when the main NewsList component has
* finished rendering using a useEffect()
*/
// eslint-disable-next-line prefer-arrow-callback, import/prefer-default-export
export const NewsItemWrapper = forwardRef(function NewsItemWrapper(
props: { item: NewsElement },
ref: Ref<HTMLDivElement>,
) {
const { item } = props;
return (
<div
id={`${newsID(item)}-wrapper`}
ref={ref}
key={newsID(item)}
className="pt-2 pb-2"
>
<NewsItem item={item} />
</div>
);
});
17 changes: 17 additions & 0 deletions src/components/news/news-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NewsElement } from '../../function/news/types';
import { SaferMarkdown } from '../markdown/safer-markdown';
import { ymd } from './util';

// eslint-disable-next-line import/prefer-default-export
export function NewsItem(props: { item: NewsElement }) {
const { item } = props;
const { meta, content } = item;
return (
<div className="border-bottom border-dark pb-1">
<SaferMarkdown>{content}</SaferMarkdown>
<div className="d-flex justify-content-end">
<span className="fs-7">{`${ymd(meta.timestamp)}`}</span>
</div>
</div>
);
}
70 changes: 70 additions & 0 deletions src/components/news/news-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useAtomValue } from 'jotai';
import { useEffect, useRef } from 'react';
import Message from '../general/message';
import { OverlayContentProps } from '../overlay/overlay';
import {
NEWS_ATOM,
newsID,
SCROLL_TO_NEWS_ATOM,
} from '../../function/news/state';
import { getStore } from '../../hooks/jotai/base';
import { NewsItemWrapper } from './news-item-wrapper';

/**
* News list visual element displaying all news available.
*
* @param props properties for this overlay content
* @returns NewsList
*/
// eslint-disable-next-line import/prefer-default-export
export function NewsList(props: OverlayContentProps) {
const { closeFunc } = props;

const scrollToNewsID = useAtomValue(SCROLL_TO_NEWS_ATOM);
const wrapperRef = useRef<HTMLDivElement>(null);

const { data, isError, isFetched, isPending } = useAtomValue(NEWS_ATOM);

const newsList =
isFetched && !isError && !isPending
? data.map((ne) => (
<NewsItemWrapper
ref={
scrollToNewsID.length > 0 && scrollToNewsID === newsID(ne)
? wrapperRef
: undefined
}
key={`${newsID(ne)}-wrapper`}
item={ne}
/>
))
: null;

useEffect(() => {
if (wrapperRef.current !== null) {
wrapperRef.current.scrollIntoView();
getStore().set(SCROLL_TO_NEWS_ATOM, '');
}
}, []);

return (
<div className="credits-container">
<h1 className="credits-title">
<Message message="news.title" />
</h1>
<div
className="parchment-box credits-text-box"
style={{ backgroundImage: '' }}
>
<div className="credits-text">{newsList}</div>
</div>
<button
type="button"
className="ucp-button credits-close"
onClick={closeFunc}
>
<Message message="close" />
</button>
</div>
);
}
4 changes: 4 additions & 0 deletions src/components/news/util.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// eslint-disable-next-line import/prefer-default-export
export function ymd(timestamp: Date) {
return `${timestamp.toLocaleDateString()}`;
}
18 changes: 18 additions & 0 deletions src/components/top-bar/news/news-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Message from '../../general/message';
import { NewsList } from '../../news/news-list';
import { setOverlayContent } from '../../overlay/overlay';

// eslint-disable-next-line import/prefer-default-export
export function NewsButton() {
return (
<button
type="button"
className="restart-button"
onClick={async () => {
setOverlayContent(NewsList, true, true);
}}
>
<Message message="news.button" />
</button>
);
}
3 changes: 3 additions & 0 deletions src/components/top-bar/top-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { OVERLAY_ACTIVE_ATOM } from '../overlay/overlay';
import CreditsButton from './credits/credits-button';
import { RestartButton } from './restart/restart-button';
import LanguageSelect from './language-select/language-select';
import { NewsButton } from './news/news-button';

// eslint-disable-next-line import/prefer-default-export
export function TopBar() {
Expand All @@ -14,6 +15,8 @@ export function TopBar() {
className="top-bar"
{...{ inert: overlayActive ? '' : undefined }} // inert is not yet supported by React
>
<NewsButton />
<span className="mx-1" />
<CreditsButton />
<span className="mx-1" />
<RestartButton />
Expand Down
126 changes: 126 additions & 0 deletions src/components/ucp-tabs/overview/news-highlights.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { useAtomValue } from 'jotai';
import { XCircleFill } from 'react-bootstrap-icons';
import { useState } from 'react';
import {
NEWS_HIGHLIGHT_ATOM,
newsID,
SCROLL_TO_NEWS_ATOM,
} from '../../../function/news/state';
import { NewsElement } from '../../../function/news/types';
import { HIDDEN_NEWS_HIGHLIGHTS_ATOM } from '../../../function/gui-settings/settings';
import { getStore } from '../../../hooks/jotai/base';
import { NewsList } from '../../news/news-list';
import { setOverlayContent } from '../../overlay/overlay';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function camelCase(s: string) {
return s.substring(0, 1).toLocaleUpperCase() + s.substring(1);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function dmy(d: Date) {
return d.toLocaleString().split(',')[0];
}

function Headline(props: { news: NewsElement }) {
const { news } = props;
const firstLine = news.content
.split('\n')
.filter((s) => s.length > 0 && s.trim().length > 0)[0];
const sanitized = firstLine.startsWith('#')
? firstLine.trim().split('#', 2)[1].trim()
: firstLine;
// const category = `${camelCase(news.meta.category)} update`;

const [isHovered, setHovered] = useState(false);

const id = newsID(news);

return (
<div
className="text-start row fs-6 pb-2 px-0"
onMouseEnter={() => {
setHovered(true);
}}
onMouseLeave={() => {
setHovered(false);
}}
>
{/* <span className="col-4">{`${category}`}</span> */}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<span
className="col mt-0 px-0"
style={{
textDecoration: isHovered ? 'underline' : '',
borderBottom: isHovered ? '1px' : '',
cursor: isHovered ? 'pointer' : 'default',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
onClick={() => {
getStore().set(SCROLL_TO_NEWS_ATOM, id);
setOverlayContent(NewsList, true, true);
}}
>{`${sanitized}`}</span>
{/* <span className="col-2">{`${dmy(news.meta.timestamp)}`}</span> */}
<span
className="col-1 px-0 mt-0"
style={{
visibility: isHovered ? 'visible' : 'hidden',
opacity: isHovered ? 1 : 0,
transition: 'visibility 0s, opacity 0.25s linear',
cursor: isHovered ? 'pointer' : 'default',
}}
>
<XCircleFill
onClick={() => {
const hidden = getStore().get(HIDDEN_NEWS_HIGHLIGHTS_ATOM);
hidden.push(news.meta.timestamp.toISOString());
getStore().set(HIDDEN_NEWS_HIGHLIGHTS_ATOM, [...hidden]);
}}
/>
</span>
</div>
);
}

// eslint-disable-next-line import/prefer-default-export, @typescript-eslint/no-unused-vars
export function NewsHighlights(props: object) {
const highlights = useAtomValue(NEWS_HIGHLIGHT_ATOM);

const hiddenHighlights = useAtomValue(HIDDEN_NEWS_HIGHLIGHTS_ATOM);

if (highlights.length === 0) {
return <div>No news today</div>;
}
return (
<div
className="text-center p-2 mt-3 pb-5"
style={{
minHeight: '0px',
width: '40%',
// backgroundColor: 'rgba(0, 0, 0, 0.5)',
backgroundImage:
'linear-gradient(to top, rgba(0,0,0,0), rgba(25,25,25,0.75))',
}}
>
<h3 className="border-bottom pb-2" style={{ marginBottom: '1.25rem' }}>
News
</h3>
<div className="h-100 px-3 pb-3" style={{ overflowY: 'scroll' }}>
{highlights
.filter(
(h) =>
hiddenHighlights.indexOf(h.meta.timestamp.toISOString()) === -1,
)
.map((h) => (
<Headline
key={`news-highlight-${h.meta.timestamp}`}
news={h as NewsElement}
/>
))}
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/ucp-tabs/overview/overview.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@

.overview__text-button {
height: 40px;
line-height: 0;
/* line-height: 0; */
min-width: 100px;
width: 40%;
vertical-align: middle;
Expand Down
2 changes: 2 additions & 0 deletions src/components/ucp-tabs/overview/overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import Logger from '../../../util/scripts/logging';
import { hintThatGameMayBeRunning } from '../../../function/game-folder/file-locks';
import { asPercentage } from '../../../tauri/tauri-http';
import { useMessage } from '../../general/message';
import { NewsHighlights } from './news-highlights';

const LOGGER = new Logger('overview.tsx');

Expand Down Expand Up @@ -437,6 +438,7 @@ export default function Overview() {
toastTitle="overview.uninstall.toast.title"
/>
<div id="decor" />
<NewsHighlights />
</div>
);
}
5 changes: 5 additions & 0 deletions src/function/gui-settings/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,8 @@ export const CONFIG_IGNORE_ERRORS = atomWithStorage(
'configMerging.ignoreErrors',
false,
);

export const HIDDEN_NEWS_HIGHLIGHTS_ATOM = atomWithStorage<string[]>(
'news.highlights.hidden',
[],
);
26 changes: 26 additions & 0 deletions src/function/news/fetching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ResponseType } from '@tauri-apps/api/http';
import { fetch } from '../../tauri/tauri-http';
import { parseNews } from './parsing';
import Logger from '../../util/scripts/logging';

const LOGGER = new Logger('news/fetching.ts');

// eslint-disable-next-line import/prefer-default-export
export async function fetchNews({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
queryKey: [, ucpVersion],
}: {
queryKey: [string, string];
}) {
const request = await fetch(
'https://raw.githubusercontent.com/UnofficialCrusaderPatch/UnofficialCrusaderPatch/refs/heads/main/NEWS.md',
{ responseType: ResponseType.Text, method: 'GET' },
);
const raw = request.data!.toString().trim();
const news = parseNews(raw);

LOGGER.msg(`News: ${JSON.stringify(news)}`).debug();
LOGGER.msg(`Raw news: ${JSON.stringify(raw)}`).debug();

return news;
}
Loading

0 comments on commit d05db34

Please sign in to comment.