-
![{alt}]()
imageRatio,
- "w-full h-auto": containerRatio <= imageRatio,
- },
- )}
- src={blobUrl}
- />
-
-
-
-
-
-
画像の説明
-
-
{alt}
-
-
-
-
+
+ {blobUrl != null && (
+ <>
+
![{alt}]()
imageRatio,
+ "w-full h-auto": containerRatio <= imageRatio,
+ },
+ )}
+ src={blobUrl}
+ />
+
+
+
+
+
+
画像の説明
+
+
{alt}
+
+
+
+
+ >
+ )}
);
};
diff --git a/application/client/src/components/foundation/InfiniteScroll.tsx b/application/client/src/components/foundation/InfiniteScroll.tsx
index 408f24c107..5f670b113d 100644
--- a/application/client/src/components/foundation/InfiniteScroll.tsx
+++ b/application/client/src/components/foundation/InfiniteScroll.tsx
@@ -7,43 +7,30 @@ interface Props {
}
export const InfiniteScroll = ({ children, fetchMore, items }: Props) => {
+ const sentinelRef = useRef
(null);
const latestItem = items[items.length - 1];
- const prevReachedRef = useRef(false);
-
useEffect(() => {
- const handler = () => {
- // 念の為 2の18乗 回、最下部かどうかを確認する
- const hasReached = Array.from(Array(2 ** 18), () => {
- return window.innerHeight + Math.ceil(window.scrollY) >= document.body.offsetHeight;
- }).every(Boolean);
+ const sentinel = sentinelRef.current;
+ if (!sentinel) return;
- // 画面最下部にスクロールしたタイミングで、登録したハンドラを呼び出す
- if (hasReached && !prevReachedRef.current) {
- // アイテムがないときは追加で読み込まない
- if (latestItem !== undefined) {
+ const observer = new IntersectionObserver(
+ (entries) => {
+ if (entries[0]?.isIntersecting && latestItem !== undefined) {
fetchMore();
}
- }
-
- prevReachedRef.current = hasReached;
- };
-
- // 最初は実行されないので手動で呼び出す
- prevReachedRef.current = false;
- handler();
+ },
+ { threshold: 0 },
+ );
- document.addEventListener("wheel", handler, { passive: false });
- document.addEventListener("touchmove", handler, { passive: false });
- document.addEventListener("resize", handler, { passive: false });
- document.addEventListener("scroll", handler, { passive: false });
- return () => {
- document.removeEventListener("wheel", handler);
- document.removeEventListener("touchmove", handler);
- document.removeEventListener("resize", handler);
- document.removeEventListener("scroll", handler);
- };
+ observer.observe(sentinel);
+ return () => observer.disconnect();
}, [latestItem, fetchMore]);
- return <>{children}>;
+ return (
+ <>
+ {children}
+
+ >
+ );
};
diff --git a/application/client/src/components/foundation/PausableMovie.tsx b/application/client/src/components/foundation/PausableMovie.tsx
index 98b0df55b0..aed1456b02 100644
--- a/application/client/src/components/foundation/PausableMovie.tsx
+++ b/application/client/src/components/foundation/PausableMovie.tsx
@@ -1,12 +1,8 @@
import classNames from "classnames";
-import { Animator, Decoder } from "gifler";
-import { GifReader } from "omggif";
-import { RefCallback, useCallback, useRef, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import { AspectRatioBox } from "@web-speed-hackathon-2026/client/src/components/foundation/AspectRatioBox";
import { FontAwesomeIcon } from "@web-speed-hackathon-2026/client/src/components/foundation/FontAwesomeIcon";
-import { useFetch } from "@web-speed-hackathon-2026/client/src/hooks/use_fetch";
-import { fetchBinary } from "@web-speed-hackathon-2026/client/src/utils/fetchers";
interface Props {
src: string;
@@ -14,77 +10,92 @@ interface Props {
/**
* クリックすると再生・一時停止を切り替えます。
+ * サーバーで H.265 MP4 に変換済みの動画を
);
})}
diff --git a/application/client/src/components/timeline/TimelineItem.tsx b/application/client/src/components/timeline/TimelineItem.tsx
index 21b88980f8..65fb1621ca 100644
--- a/application/client/src/components/timeline/TimelineItem.tsx
+++ b/application/client/src/components/timeline/TimelineItem.tsx
@@ -56,6 +56,7 @@ export const TimelineItem = ({ post }: Props) => {
>