Skip to content

Commit

Permalink
Merge pull request #6 from azu/streaming
Browse files Browse the repository at this point in the history
refactor(web): Suspense + Streaming
  • Loading branch information
azu authored Feb 25, 2024
2 parents 5e4bd84 + 1e2bdd5 commit 5d665d8
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 194 deletions.
8 changes: 4 additions & 4 deletions web/app/client/CompositionInput.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"use client";
import { CSSProperties, useCallback, useState } from "react";
import React, { CSSProperties, useCallback, useState } from "react";

export function CompositionInput(props: { style?: CSSProperties; value: string; onInput: (value: string) => void }) {
const [inputValue, setInputValue] = useState(props.value);
const [isComposing, setIsComposing] = useState(false);
const onInput = useCallback(
(event) => {
(event: React.FormEvent<HTMLInputElement>) => {
const value = event.currentTarget.value;
setInputValue(value);
if (!isComposing) {
Expand All @@ -14,10 +14,10 @@ export function CompositionInput(props: { style?: CSSProperties; value: string;
},
[isComposing]
);
const onCompositionStart = useCallback((e) => {
const onCompositionStart = useCallback(() => {
setIsComposing(true);
}, []);
const onCompositionEnd = useCallback((event) => {
const onCompositionEnd = useCallback((event: React.FormEvent<HTMLInputElement>) => {
setIsComposing(false);
const value = event.currentTarget.value;
setInputValue(value);
Expand Down
11 changes: 7 additions & 4 deletions web/app/client/SearchMore.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import { LineTweetResponse } from "../server/search";
import { useMemo, useTransition } from "react";
import { FetchS3SelectResult, LineTweetResponse } from "../server/search";
import { use, useMemo, useTransition } from "react";
import { useTypeUrlSearchParams } from "../lib/useTypeUrlSearchParams";
import { HomPageSearchParam } from "../page";

Expand All @@ -27,8 +27,11 @@ export const useSearchMore = (props: { searchResults: LineTweetResponse[] }) =>
isLoadingMore
} as const;
};
export const SearchMore = (props: { searchResults: LineTweetResponse[] }) => {
const { handlers, isLoadingMore } = useSearchMore(props);
export const SearchMore = (props: { retPromise: Promise<FetchS3SelectResult> }) => {
const { results } = use(props.retPromise);
const { handlers, isLoadingMore } = useSearchMore({
searchResults: results
});
return (
<div
style={{
Expand Down
30 changes: 20 additions & 10 deletions web/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { fetchS3Select } from "./server/search";
import React, { Suspense, use } from "react";
import { fetchS3Select, FetchS3SelectResult } from "./server/search";
import { SiTwitter } from "react-icons/si";
import { SearchResultContent } from "./server/SearchResult";
import { SearchBox } from "./client/SearchBox";
Expand All @@ -14,13 +14,18 @@ export type HomPageSearchParam = {
timestamp?: string;
};

const HitCount = (props: { retPromise: Promise<FetchS3SelectResult> }) => {
const ret = use(props.retPromise);
return <span>Hit: {ret.results.length}</span>;
};

async function HomePage({
searchParams
}: {
// https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional
searchParams: HomPageSearchParam;
}) {
const ret = await fetchS3Select({
const retPromise = fetchS3Select({
max: searchParams.max ? Number(searchParams.max) : 20,
query: searchParams.q ?? "",
afterTimestamp: searchParams.timestamp ? Number(searchParams.timestamp) : undefined
Expand Down Expand Up @@ -60,18 +65,23 @@ async function HomePage({
</a>
<SearchBox query={searchParams.q} />
</div>
<div>{/*<span>Hit: {sortedSearchResults.length}</span>*/}</div>
<div>
<Suspense fallback={<span>Hit: …</span>}>
<HitCount retPromise={retPromise} />
</Suspense>
</div>
</div>
<SearchResultContentWrapper>
<SearchResultContent
isFetching={false}
searchResults={ret.results}
screenName={searchParams.screen_name ?? ""}
/>
<Suspense fallback={<>Loading…</>}>
<SearchResultContent retPromise={retPromise} screenName={searchParams.screen_name ?? ""} />
</Suspense>
</SearchResultContentWrapper>
<SearchMore searchResults={ret.results} />
<Suspense>
<SearchMore retPromise={retPromise} />
</Suspense>
</div>
</TransitionContextProvider>
);
}

export default HomePage;
177 changes: 82 additions & 95 deletions web/app/server/SearchResult.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { LineTweetResponse } from "./search";
import { FetchS3SelectResult, LineTweetResponse } from "./search";
import { FaSpinner } from "react-icons/fa";
import dayjs from "dayjs";
import { MdPerson, MdUpdate } from "react-icons/md";
import utc from "dayjs/plugin/utc";
import { ReactElement } from "react";
import { ReactElement, use } from "react";

dayjs.extend(utc);
const StatusLink = (props: { itemId: string; children: ReactElement }) => {
Expand All @@ -26,106 +26,93 @@ const StatusLink = (props: { itemId: string; children: ReactElement }) => {
);
};

export function SearchResultContent(props: {
isFetching: boolean;
searchResults: LineTweetResponse[];
screenName: string;
}) {
export function SearchResultContent(props: { retPromise: Promise<FetchS3SelectResult>; screenName: string }) {
const { results: searchResults } = use(props.retPromise);
return (
<div>
{props.isFetching ? (
<FaSpinner
size={24}
className={"fa-spin"}
style={{
margin: "8px 0"
}}
/>
) : (
<ul
style={{
listStyle: "none",
padding: 0
}}
>
{props.searchResults.map((item) => {
const day = dayjs.utc(item.timestamp);
const isTwitter = !item.id.startsWith("at://");
return (
<li
key={item.id}
className={"Tweet-Item"}
<ul
style={{
listStyle: "none",
padding: 0
}}
>
{searchResults.map((item) => {
const day = dayjs.utc(item.timestamp);
const isTwitter = !item.id.startsWith("at://");
return (
<li
key={item.id}
className={"Tweet-Item"}
style={{
paddingBottom: "1rem",
display: "flex",
flexDirection: "column",
border: "1px solid rgb(235, 238, 240)",
boxSizing: "border-box"
}}
>
<span
style={{
paddingBottom: "1rem",
fontSize: "16px",
display: "flex",
flexDirection: "column",
border: "1px solid rgb(235, 238, 240)",
boxSizing: "border-box"
alignContent: "center",
alignItems: "center"
}}
>
<span
style={{
fontSize: "16px",
display: "flex",
alignContent: "center",
alignItems: "center"
}}
>
<StatusLink itemId={item.id}>
<time dateTime={day.toISOString()}>{day.format("YYYY-MM-DD HH:mm")}</time>
</StatusLink>
{isTwitter && (
<>
<a
href={`https://twitter.com/search?q=${encodeURIComponent(
"filter:follows since:" +
day.format("YYYY-MM-DD_HH:mm:ss_UTC") +
" until:" +
day.add(1, "day").format("YYYY-MM-DD_HH:mm:ss_UTC") +
""
)}&src=typed_query&f=live`}
title={"Search this date"}
target={"_blank"}
className="Icon-Center"
>
<MdUpdate size={16} style={{}} />
</a>
<StatusLink itemId={item.id}>
<time dateTime={day.toISOString()}>{day.format("YYYY-MM-DD HH:mm")}</time>
</StatusLink>
{isTwitter && (
<>
<a
href={`https://twitter.com/search?q=${encodeURIComponent(
"filter:follows since:" +
day.format("YYYY-MM-DD_HH:mm:ss_UTC") +
" until:" +
day.add(1, "day").format("YYYY-MM-DD_HH:mm:ss_UTC") +
""
)}&src=typed_query&f=live`}
title={"Search this date"}
target={"_blank"}
className="Icon-Center"
>
<MdUpdate size={16} style={{}} />
</a>

<a
href={`https://twitter.com/search?q=${encodeURIComponent(
"from:" +
props.screenName +
" since:" +
day.format("YYYY-MM-DD_HH:mm:ss_UTC") +
" until:" +
day.add(1, "day").format("YYYY-MM-DD_HH:mm:ss_UTC") +
""
)}&src=typed_query&f=live`}
title={"Search this date from me"}
target={"_blank"}
className="Icon-Center"
>
<MdPerson size={16} />
</a>
</>
)}
</span>
<p
style={{
margin: 0,
padding: "0 12px",
whiteSpace: "pre-wrap",
wordBreak: "break-word"
}}
dangerouslySetInnerHTML={{
__html: item.html
}}
/>
</li>
);
})}
</ul>
)}
<a
href={`https://twitter.com/search?q=${encodeURIComponent(
"from:" +
props.screenName +
" since:" +
day.format("YYYY-MM-DD_HH:mm:ss_UTC") +
" until:" +
day.add(1, "day").format("YYYY-MM-DD_HH:mm:ss_UTC") +
""
)}&src=typed_query&f=live`}
title={"Search this date from me"}
target={"_blank"}
className="Icon-Center"
>
<MdPerson size={16} />
</a>
</>
)}
</span>
<p
style={{
margin: 0,
padding: "0 12px",
whiteSpace: "pre-wrap",
wordBreak: "break-word"
}}
dangerouslySetInnerHTML={{
__html: item.html
}}
/>
</li>
);
})}
</ul>
</div>
);
}
3 changes: 2 additions & 1 deletion web/app/server/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const escapeLike = (s: string) => {
);
};

export type FetchS3SelectResult = { results: LineTweetResponse[]; stats: StatsEvent["Details"] };
export const fetchS3Select = async ({
query,
max,
Expand All @@ -67,7 +68,7 @@ export const fetchS3Select = async ({
query: string;
max: number;
afterTimestamp?: number;
}): Promise<{ results: LineTweetResponse[]; stats: StatsEvent["Details"] }> => {
}): Promise<FetchS3SelectResult> => {
const queries = query
.split(/\s+/)
.map((query) => query.trim())
Expand Down
11 changes: 3 additions & 8 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,11 @@
"next": "^14.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.0.1",
"regex-combiner": "^1.0.1",
"split2": "^3.2.2",
"sqlstring": "^2.3.3",
"use-debounce": "^6.0.1"
"react-icons": "^5.0.1"
},
"devDependencies": {
"@types/react": "^17.0.11",
"@types/split2": "^3.2.0",
"@types/sqlstring": "^2.3.0",
"@types/react": "^18.2.58",
"@types/node": "^20.11.20",
"dotenv": "^10.0.0",
"typescript": "^5.3.3"
}
Expand Down
Loading

0 comments on commit 5d665d8

Please sign in to comment.