Skip to content

Commit

Permalink
Documentation in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthewTurk247 committed Oct 6, 2023
1 parent 450b8d6 commit 7096ae5
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 67 deletions.
64 changes: 39 additions & 25 deletions components/common/Wildcard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import { Dimensions, Image, PixelRatio, StyleSheet, View } from "react-native";
import { Spacing } from "../../utils/constants";
import { formatDate, itemize } from "../../utils/format";

const { width, height } = Dimensions.get("window");
const { width } = Dimensions.get("window");
const pixelRatio = PixelRatio.get();

/**
* `Header` is a sub-component of `Wildcard` that displays the article title, date, and image.
* @component
* @param {Object} props
* @param {string} props.title - The title of the article.
* @param {boolean} props.verbose - A flag indicating whether to include the excerpt in the card.
* @param {string} props.date - The publication date.
Expand Down Expand Up @@ -40,6 +41,7 @@ const Header = ({ title, verbose, date, media }) => (
/**
* `Footer` is a sub-component of `Wildcard` that displays the article byline and section.
* @component
* @param {Object} props
* @param {string} props.byline - The author's name(s) in a displayable format.
* @param {string} props.section - The name of the section to which the article belongs.
*/
Expand All @@ -54,30 +56,42 @@ const Footer = ({ byline, section }) => (
</View>
);

export default function Wildcard({ navigation, articles, random, verbose, title, item, index }) {
return item.title && (
<Card
style={styles.card}
accessibilityLabel={`Article titled ${decode(item.title.rendered)}`}
accessibilityHint="Navigates to a view with full text for article"
header={
<Header
verbose={verbose}
title={decode(item.title.rendered)}
date={item.date}
media={item["_embedded"]?.["wp:featuredmedia"]?.[0]}
/>
}
footer={
<Footer
byline={itemize(item.parsely?.meta?.creator?.map((name) => name.toUpperCase()))}
section={item.parsely?.meta?.articleSection}
/>
}
onPress={() => navigation.push("Post", { article: item, sourceName: title })}
>
<Text style={{ marginHorizontal: -4 }}>{decode(item.excerpt.rendered.slice(3, -5))}</Text>
</Card>
/**
* Displays a `Card` preview for an article or some other related form of content.
* @component
* @param {Object} props
* @param {Object} props.navigation - The navigation object used for navigating between screens.
* @param {boolean} props.verbose - A flag indicating whether to include the excerpt in the card.
* @param {string} props.title - The headline.
* @param {import("../../utils/model").WordPressPost} props.item - The article object.
* @param {number} props.index - The index of the article in a parent view. Currently unused in the implementation.
*/
export default function Wildcard({ navigation, verbose, title, item, index }) {
return (
item.title && (
<Card
style={styles.card}
accessibilityLabel={`Article titled ${decode(item.title.rendered)}`}
accessibilityHint="Navigates to a view with full text for article"
header={
<Header
verbose={verbose}
title={decode(item.title.rendered)}
date={item.date}
media={item["_embedded"]?.["wp:featuredmedia"]?.[0]}
/>
}
footer={
<Footer
byline={itemize(item.parsely?.meta?.creator?.map((name) => name.toUpperCase()))}
section={item.parsely?.meta?.articleSection}
/>
}
onPress={() => navigation.push("Post", { article: item, sourceName: title })}
>
<Text style={{ marginHorizontal: -4 }}>{decode(item.excerpt.rendered.slice(3, -5))}</Text>
</Card>
)
);
}

Expand Down
1 change: 1 addition & 0 deletions components/screens/Home.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const FirstPage = React.memo(({ data, articles, navigation, deviceType }) => {
* It also contains pagination logic to fetch new data when the end of the list is reached.
*
* @component
* @param {Object} props
* @param {Object} props.navigation - The navigation object used by React Navigation.
*/
export default function Home({ navigation }) {
Expand Down
5 changes: 2 additions & 3 deletions components/screens/Post.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as Device from "expo-device";
import { StatusBar } from "expo-status-bar";
import { decode } from "html-entities";
import React, { useContext, useEffect, useState } from "react";
import { Dimensions, Linking, PixelRatio, StyleSheet, Touchable, TouchableHighlight, TouchableOpacity, View, useColorScheme } from "react-native";
import { Dimensions, Linking, PixelRatio, StyleSheet, View, useColorScheme } from "react-native";
import { ImageHeaderScrollView } from "react-native-image-header-scroll-view";
import Content, { defaultSystemFonts, useInternalRenderer } from "react-native-render-html";
import WebView from "react-native-webview";
Expand Down Expand Up @@ -45,9 +45,8 @@ export default function Post({ route, navigation }) {
const pruned = url.slice(-1) === "/" ? url.slice(0, -1) : url;
const slug = pruned.split("/").at(-1);

// Hopefully this doesn't take too long to load. Might have to preload.

if (url.match(/stanforddaily.com\/\d{4}\/\d{2}\/\d{2}\/(.*)/)) {
// Hopefully this doesn't take too long to load. Might have to preload.
Model.posts()
.slug(slug)
.embed()
Expand Down
53 changes: 14 additions & 39 deletions components/screens/Section.jsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,17 @@
import { Layout, List, Text, Tab, TabBar } from "@ui-kitten/components";
import { Layout, List, Tab, TabBar } from "@ui-kitten/components";
import { DeviceType } from "expo-device";
import React, { useCallback, useContext, useEffect, useState } from "react";
import { ActivityIndicator, ScrollView, StyleSheet, View } from "react-native";
import PagerView from "react-native-pager-view";

import { ThemeContext } from "../../theme-context";
import { Spacing } from "../../utils/constants";
import { throttle } from "../../utils/format";
import Model from "../../utils/model";
import Wildcard from "../common/Wildcard";

const BATCH_SIZE = 16;

const MemoizedWildcard = React.memo(Wildcard);
// Util function to throttle events
const throttle = (func, limit) => {
let inThrottle;
return function () {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
};

/**
* The `Section` screen displays a list of articles from a specific category.
Expand All @@ -47,21 +34,6 @@ export default function Section({ route, navigation }) {
// const [articlesLoading, setArticlesLoading] = useState(false);
const [selection, setSelection] = useState(0);
const [allArticles, setAllArticles] = useState(seed);
// Before loading more articles, we calculate the number of pages we can skip when calling the API.
// Since there might already be a chronology of several articles passed in from the seed data, there's no need to fetch them again.
/*const [pageNumbers, setPageNumbers] = useState({
...{ [category.slug]: seed.length === 0 ? 1 : Math.max(0, Math.floor(seed.length / BATCH_SIZE) - 1) },
...Object.fromEntries(Object.values(category.desks ?? {}).map((desk) => [desk.slug, 1])),
});*/
/*const [articles, setArticles] = useState({
...{ [category.slug]: seed },
...Object.fromEntries(
Object.values(category.desks ?? {}).map((desk) => [
desk.slug,
seed.filter((item) => item.categories.includes(desk.id)),
])
),
});*/
// The `theme` and `deviceType` are used to determine the number of columns in the list of articles.
const { theme, deviceType } = useContext(ThemeContext);
const columnCount = deviceType === DeviceType.PHONE ? 1 : 2;
Expand All @@ -84,6 +56,7 @@ export default function Section({ route, navigation }) {
const [articlesLoading, setArticlesLoading] = useState(false);
const [articles, setArticles] = useState(sectionArticles);
// Since there might already be a chronology of several articles passed in from the seed data, there's no need to fetch them again.
// Right now the page numbers are a little out of sync, so that needs to be fixed ASAP.
const [pageNumber, setPageNumber] = useState(basePageCount);

const fetchResults = async () => {
Expand Down Expand Up @@ -127,15 +100,17 @@ export default function Section({ route, navigation }) {
setPageNumber(pageNumber + 1);
}
}}
renderItem={({ item, index }) => (
<MemoizedWildcard
key={`${subcategory.id}-${item.id}`}
item={item}
index={index}
navigation={navigation}
verbose
/>
)}
renderItem={({ item, index }) =>
React.memo(
<Wildcard
key={`${subcategory.id}-${item.id}`}
item={item}
index={index}
navigation={navigation}
verbose
/>
)
}
ListFooterComponent={() => {
if (!possiblyReachedEnd || articlesLoading) {
return <ActivityIndicator />;
Expand Down
24 changes: 24 additions & 0 deletions utils/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,27 @@ export function parsePipedHeadline(headline) {
// Return the whole headline if there's no pipe character.
return headline;
}

/**
* Utility function for throttling events, particularly when many events may incidentally be fired quick succession.
* Ultimately, it limits how often `func` can be called.
*
* Wraps the given function in logic that ensures it cannot be invoked more frequently than the given limit.
* Useful for limiting events that could fire rapidly like scroll and resize handlers.
*
* @function
* @param {Function} func - The function to throttle.
* @param {number} limit - The minimum interval allowed between calls (in milliseconds).
* @returns {Function} The throttled function wrapper.
*/
export const throttle = (func, limit) => {
let inThrottle;
return function (...args) {
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
};

0 comments on commit 7096ae5

Please sign in to comment.