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
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;
|
|
|