import React, { useRef, useState } from "react"; import { Col, Row } from "antd"; import { EVENT_PRIORITY, Timeline, TimelineItem, TimestampEvent, } from "../types/types"; import FeedItem from "./FeedItem"; import connection, { calculateRoomTime, useRoomRunningAndReadyChanged, useRoomTime, useTimeline, } from "../lib/Connection"; import { useResize, useUpdateAfterDelay } from "../util/hooks"; import CSSTransition from "react-transition-group/CSSTransition"; import TransitionGroup from "react-transition-group/TransitionGroup"; import NextShot from "./NextShot"; import ShotsTaken from "./ShotsTaken"; import Player from "./Player"; import "../css/feed.css"; import FeedTicker from "./FeedTicker"; declare global { interface Window { __feedShakeDebug?: boolean; } } function getNextItemDelay(timeline: Timeline | null, defaultDelay = 500) { if (!timeline) { return defaultDelay; } const time = calculateRoomTime(); const nextItem = timeline.itemAfterTime(time); if (!nextItem) { return defaultDelay; } const speedFactor = connection.room.get()?.speedFactor || 1; return (nextItem.timestamp * 1000 - time) / speedFactor; } /** * This map relates back an event to the index of the item displayed from * top to bottom. We can use this to calculate opacity. */ function calculateKeyToIndexMap(feed: TimelineItem[]) { const keyToIndex: Map = new Map(); let totalEvents = 0; for (const item of feed) { totalEvents += item.events.length; } let totalIdx = 0; for (const item of feed) { for (let eventIdx = 0; eventIdx < item.events.length; eventIdx++) { keyToIndex.set(`${item.timestamp}.${eventIdx}`, totalEvents - ++totalIdx); } } return keyToIndex; } /** * Makes sure 'shot' is always top-most within an timeline item */ function sortEvents(events: TimestampEvent[]) { return events.sort((a, b) => { return EVENT_PRIORITY.indexOf(b.type) - EVENT_PRIORITY.indexOf(a.type); }); } const Feed = () => { const timeline = useTimeline(); useRoomRunningAndReadyChanged(); useResize(); useRoomTime(); const feedElement = useRef(null); const [lastShotId, setLastShotId] = useState(null); useUpdateAfterDelay(getNextItemDelay(timeline)); if (!timeline) { throw new TypeError("Feed without timeline."); } const time = calculateRoomTime(); const liveFeed = timeline.feed.filter((item) => { return item.timestamp * 1000 <= time; }); const keyToIndex: Map = calculateKeyToIndexMap(liveFeed); const lastEvent = timeline.eventBeforeTime(time, "shot"); if (lastEvent && lastShotId !== lastEvent?.id) { setLastShotId(lastEvent.id); if (feedElement.current) { // Let the browser first do the heavy dom stuff to avoid lagging our // animation. setTimeout(doSingleShake, 100); } } function doSingleShake() { if (!feedElement.current) return; if (feedElement.current.getAnimations().length > 0) return; if (feedElement.current.classList.contains("feed-shot-shake")) { feedElement.current.getAnimations().forEach((i) => i.finish()); feedElement.current.classList.remove("feed-shot-shake"); setTimeout(doSingleShake, 0); return; } feedElement.current.classList.add("feed-shot-shake"); const el = feedElement.current; const listener = function () { el.classList.remove("feed-shot-shake"); el.removeEventListener("animationend", listener); }; el.addEventListener("animationend", listener); } function itemOpacity(itemKey: string) { // This is a bit of a hack but works better than trying to do this in css. // This fades out the elements the lower the element is on the screen. // Ticker is only visible on large screens, the ticker obstructs some of // the items. If it is not visible we simply take body height, otherwise // we use the height where the ticker starts. const tickerHeight = document .querySelector(".ticker-container") ?.getBoundingClientRect().y; const bodyHeight = document.body.clientHeight; const totalHeight = tickerHeight ? tickerHeight : bodyHeight; // Start at top most item, figure out which index is the first // that is NOT visible. const items = document.querySelectorAll(".feed-item-container"); let i = items.length - 1; for (; i > 0; i--) { const rect = items[i].getBoundingClientRect(); // If the bottom of this element is below the screen, we declare it // not-visible. if (rect.y + rect.height >= totalHeight) { break; } } const totalItemsOnScreen = items.length - i; const index = keyToIndex.get(itemKey) ?? 0; let x = index / (totalItemsOnScreen - 1.8); x = Math.min(0.9, Math.max(0, x)); return 1 - Math.pow(x, 4); } const debug = true; if (debug) { if (!window["__feedShakeDebug"]) { window["__feedShakeDebug"] = true; window.document.documentElement.addEventListener("keydown", (e) => { if (e.keyCode === 67) { // c doSingleShake(); } }); } } return (
{liveFeed.map((item) => sortEvents(item.events).map((event, j) => (
)) )}
); }; export default Feed;