Skip to content

Commit

Permalink
Merge pull request #138 from eurofurence/refactor/announcement-refact…
Browse files Browse the repository at this point in the history
…roing

refactor: announcement display
  • Loading branch information
Requinard authored Aug 25, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 746dff7 + c8757cb commit d4330e9
Showing 26 changed files with 522 additions and 169 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -116,7 +116,7 @@
"expo-updates": "~0.25.21",
"expo-web-browser": "~13.0.3",
"firebase": "^10.1.0",
"fuse.js": "^6.6.2",
"fuse.js": "^7.0.0",
"i18next": "^23.4.1",
"intl-pluralrules": "^2.0.1",
"lodash": "^4.17.21",
12 changes: 6 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

102 changes: 81 additions & 21 deletions src/components/announce/AnnouncementCard.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,95 @@
import { FC } from "react";
import { StyleSheet, View } from "react-native";
import { ImageBackground } from "expo-image";
import moment, { Moment } from "moment";
import React, { FC } from "react";
import { StyleSheet, View, ViewStyle } from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";

import { colorForArea } from "./utils";
import { useThemeBackground, useThemeName } from "../../hooks/themes/useThemeHooks";
import { AnnouncementDetails } from "../../store/eurofurence/types";
import { Image } from "../generic/atoms/Image";
import { appStyles } from "../AppStyles";
import { Label } from "../generic/atoms/Label";
import { MarkdownContent } from "../generic/atoms/MarkdownContent";
import { Card } from "../generic/containers/Card";

export const AnnouncementCard: FC<{ announcement: AnnouncementDetails }> = ({ announcement }) => {
export type AnnouncementDetailsInstance = {
details: AnnouncementDetails;
time: string;
};

/**
* Creates the announcement instance props for an upcoming or running announcement.
* @param details The details to use.
* @param now The moment to check against.
*/
export function announcementInstanceForAny(details: AnnouncementDetails, now: Moment): AnnouncementDetailsInstance {
return { details, time: moment.duration(moment(details.ValidFromDateTimeUtc).diff(now)).humanize(true) };
}

export type AnnouncementCardProps = {
containerStyle?: ViewStyle;
style?: ViewStyle;
announcement: AnnouncementDetailsInstance;
onPress?: (announcement: AnnouncementDetails) => void;
onLongPress?: (announcement: AnnouncementDetails) => void;
};

export const AnnouncementCard: FC<AnnouncementCardProps> = ({ containerStyle, style, announcement, onPress, onLongPress }) => {
// Dependent and independent styles.
const styleContainer = useThemeBackground("background");
const saturationValue = useThemeName() === "dark" ? 0.5 : 0.7;
const stylePre = useThemeBackground("primary");
const styleAreaIndicator = { backgroundColor: colorForArea(announcement.details.Area, saturationValue, 0.76) };
return (
<Card>
<View style={styles.margin}>
<Label type="h3">{announcement.Title}</Label>
<Label type="caption">
{announcement.Area} - {announcement.Author}
<TouchableOpacity
containerStyle={containerStyle}
style={[styles.container, appStyles.shadow, styleContainer, style]}
onPress={() => onPress?.(announcement.details)}
onLongPress={() => onLongPress?.(announcement.details)}
>
<ImageBackground style={[styles.pre, stylePre]} source={announcement.details.Image?.Url ?? null}>
<View style={[styles.areaIndicator, styleAreaIndicator]} />
</ImageBackground>

<View style={styles.main}>
<Label style={styles.title} type="h3">
{announcement.details.NormalizedTitle}
</Label>
<Label style={styles.tag} type="regular" ellipsizeMode="head" numberOfLines={1}>
{announcement.time} - {announcement.details.Area}
</Label>
</View>

<MarkdownContent>{announcement.Content}</MarkdownContent>

{announcement.Image && <Image source={announcement.Image.Url} style={styles.image} priority="high" />}
</Card>
</TouchableOpacity>
);
};

const styles = StyleSheet.create({
margin: {
marginBottom: 5,
container: {
minHeight: 80,
marginVertical: 15,
borderRadius: 16,
overflow: "hidden",
flexDirection: "row",
},
pre: {
overflow: "hidden",
width: 70,
alignItems: "center",
justifyContent: "center",
},
areaIndicator: {
position: "absolute",
top: 0,
right: 0,
bottom: 0,
width: 5,
},
main: {
flex: 1,
padding: 12,
},
title: {
flex: 1,
},
image: {
width: "100%",
height: "auto",
tag: {
textAlign: "right",
},
});
72 changes: 52 additions & 20 deletions src/components/announce/AnnouncementList.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,61 @@
import { FlashList } from "@shopify/flash-list";
import { Moment } from "moment";
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { FC, ReactElement } from "react";
import { StyleSheet } from "react-native";

import { AnnouncementCard } from "./AnnouncementCard";
import { useAppSelector } from "../../store";
import { selectActiveAnnouncements } from "../../store/eurofurence/selectors/announcements";
import { Label } from "../generic/atoms/Label";
import { Section } from "../generic/atoms/Section";
import { AnnouncementCard, AnnouncementDetailsInstance } from "./AnnouncementCard";
import { useThemeName } from "../../hooks/themes/useThemeHooks";
import { AnnounceListProps } from "../../routes/announce/AnnounceList";
import { useSynchronizer } from "../sync/SynchronizationProvider";

export type AnnouncementListProps = {
now: Moment;
navigation: AnnounceListProps["navigation"];
leader?: ReactElement;
announcements: AnnouncementDetailsInstance[];
empty?: ReactElement;
trailer?: ReactElement;
padEnd?: boolean;
};
export const AnnouncementList: FC<AnnouncementListProps> = ({ now }) => {
const { t } = useTranslation("Announcements");
const announcements = useAppSelector((state) => selectActiveAnnouncements(state, now));

if (!announcements.length) {
return null;
}

export const AnnouncementList: FC<AnnouncementListProps> = ({ navigation, leader, announcements, empty, trailer, padEnd = true }) => {
const theme = useThemeName();
const synchronizer = useSynchronizer();
return (
<>
<Section title={t("sectionTitle")} subtitle={t("sectionSubtitle")} icon="newspaper" />

{announcements.length === 0 ? <Label mb={15}>{t("noAnnouncements")}</Label> : announcements.map((it) => <AnnouncementCard announcement={it} key={it.Id} />)}
</>
<FlashList
refreshing={synchronizer.isSynchronizing}
onRefresh={synchronizer.synchronize}
contentContainerStyle={padEnd ? styles.container : undefined}
scrollEnabled={true}
ListHeaderComponent={leader}
ListFooterComponent={trailer}
ListEmptyComponent={empty}
data={announcements}
keyExtractor={(item) => item.details.Id}
renderItem={({ item }) => {
return (
<AnnouncementCard
containerStyle={styles.item}
key={item.details.Id}
announcement={item}
onPress={(announcement) =>
navigation.navigate("AnnounceItem", {
id: announcement.Id,
})
}
/>
);
}}
estimatedItemSize={110}
extraData={theme}
/>
);
};

const styles = StyleSheet.create({
item: {
paddingHorizontal: 20,
},
container: {
paddingBottom: 100,
},
});
76 changes: 76 additions & 0 deletions src/components/announce/RecentAnnouncements.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { orderBy } from "lodash";
import { Moment } from "moment";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { StyleSheet, View } from "react-native";

import { AnnouncementCard, announcementInstanceForAny } from "./AnnouncementCard";
import { useAppNavigation } from "../../hooks/nav/useAppNavigation";
import { useAppSelector } from "../../store";
import { selectActiveAnnouncements } from "../../store/eurofurence/selectors/announcements";
import { Section } from "../generic/atoms/Section";
import { Button } from "../generic/containers/Button";

const recentLimit = 2;

export type RecentAnnouncementsProps = {
now: Moment;
};

/**
* Shows the two latest announcements and a button to open all of them,
* @param now The current time.
* @constructor
*/
export const RecentAnnouncements = ({ now }: RecentAnnouncementsProps) => {
const navigation = useAppNavigation("Areas");
const { t } = useTranslation("Home");

// Get all active announcements.
const announcements = useAppSelector((state) => selectActiveAnnouncements(state, now));

// Select to the recent announcements.
const recentAnnouncements = useMemo(
() =>
orderBy(announcements, "ValidFromDateTimeUtc", "desc")
.slice(0, recentLimit)
.map((details) => announcementInstanceForAny(details, now)),
[announcements, now],
);

// Skip if empty.
if (recentAnnouncements.length === 0) {
return null;
}

return (
<>
<Section title={t("recent_announcements")} subtitle={t("announcementsTitle", { count: announcements.length })} icon="newspaper" />
<View style={styles.condense}>
{recentAnnouncements.map((item) => (
<AnnouncementCard
key={item.details.Id}
announcement={item}
onPress={(announcement) =>
navigation.navigate("AnnounceItem", {
id: announcement.Id,
})
}
/>
))}
</View>
<Button style={styles.button} onPress={() => navigation.navigate("AnnounceList")} outline>
{t("view_all_announcements")}
</Button>
</>
);
};

const styles = StyleSheet.create({
condense: {
marginVertical: -15,
},
button: {
marginTop: 20,
},
});
18 changes: 18 additions & 0 deletions src/components/announce/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { colorFromHsv } from "../../util/colorFromHsv";

/**
* Computes a color to display for an announcement area.
* @param area Area name.
* @param s Saturation to use.
* @param v Value to use.
*/
export const colorForArea = (area: string, s: number, v: number) => {
// Hash, then xor-shift. Use as HSV index.
let n = 1;
for (let i = 0; i < area.length; i++) n = (n * 7 + area.charCodeAt(i)) % Number.MAX_SAFE_INTEGER;
n += 0x6d2b79f5;
n = Math.imul(n ^ (n >>> 15), n | 1);
n ^= n + Math.imul(n ^ (n >>> 7), n | 61);
n = (n ^ (n >>> 14)) >>> 0;
return colorFromHsv((n % (360 / 15)) * 15, s, v);
};
33 changes: 21 additions & 12 deletions src/components/events/CurrentEventsList.tsx
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ import { chain } from "lodash";
import { Moment } from "moment";
import { FC, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { StyleSheet, View } from "react-native";

import { EventCard, eventInstanceForAny } from "./EventCard";
import { useAppNavigation } from "../../hooks/nav/useAppNavigation";
@@ -36,18 +37,26 @@ export const CurrentEventList: FC<CurrentEventListProps> = ({ now }) => {
return (
<>
<Section title={t("current_title")} subtitle={t("current_subtitle")} icon="clock" />
{events.map((event) => (
<EventCard
key={event.details.Id}
event={event}
type="duration"
onPress={(event) =>
navigation.navigate("Event", {
id: event.Id,
})
}
/>
))}
<View style={styles.condense}>
{events.map((event) => (
<EventCard
key={event.details.Id}
event={event}
type="duration"
onPress={(event) =>
navigation.navigate("Event", {
id: event.Id,
})
}
/>
))}
</View>
</>
);
};

const styles = StyleSheet.create({
condense: {
marginVertical: -15,
},
});
Loading

0 comments on commit d4330e9

Please sign in to comment.