Kopie van https://gitlab.com/studieverenigingvia/ict/centurion met een paar aanpassingen
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

220 lines
6.2 KiB

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<string, number> = 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<HTMLDivElement>(null);
const [lastShotId, setLastShotId] = useState<string | null>(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<string, number> = 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 (
<div className="feed" ref={feedElement}>
<Row className="feed-items">
<Col span={12} md={4} className="sider">
<NextShot />
<Player />
</Col>
<Col span={12} md={{ span: 4, push: 16 }} className="sider">
<ShotsTaken />
</Col>
<Col span={24} md={{ span: 16, pull: 4 }}>
<TransitionGroup className="feed-reverse">
{liveFeed.map((item) =>
sortEvents(item.events).map((event, j) => (
<CSSTransition
timeout={300}
classNames="fade"
key={`${item.timestamp}.${j}`}
>
<div
className="feed-item-container"
style={{ opacity: itemOpacity(`${item.timestamp}.${j}`) }}
>
<FeedItem item={event} />
</div>
</CSSTransition>
))
)}
</TransitionGroup>
</Col>
</Row>
<Row className="ticker">
<FeedTicker />
</Row>
</div>
);
};
export default Feed;