@streamplace/components 0.6.37 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/chat/chat-box.js +109 -0
- package/dist/components/chat/chat-message.js +76 -0
- package/dist/components/chat/chat.js +56 -0
- package/dist/components/chat/mention-suggestions.js +39 -0
- package/dist/components/chat/mod-view.js +33 -0
- package/dist/components/mobile-player/fullscreen.js +69 -0
- package/dist/components/mobile-player/fullscreen.native.js +151 -0
- package/dist/components/mobile-player/player.js +103 -0
- package/dist/components/mobile-player/props.js +1 -0
- package/dist/components/mobile-player/shared.js +51 -0
- package/dist/components/mobile-player/ui/countdown.js +79 -0
- package/dist/components/mobile-player/ui/index.js +6 -0
- package/dist/components/mobile-player/ui/input.js +38 -0
- package/dist/components/mobile-player/ui/metrics.js +40 -0
- package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
- package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
- package/dist/components/mobile-player/ui/viewers.js +19 -0
- package/dist/components/mobile-player/use-webrtc.js +232 -0
- package/dist/components/mobile-player/video.js +375 -0
- package/dist/components/mobile-player/video.native.js +255 -0
- package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
- package/dist/components/mobile-player/webrtc-primitives.js +25 -0
- package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
- package/dist/components/ui/button.js +220 -0
- package/dist/components/ui/dialog.js +203 -0
- package/dist/components/ui/dropdown.js +148 -0
- package/dist/components/ui/icons.js +22 -0
- package/dist/components/ui/index.js +22 -0
- package/dist/components/ui/input.js +202 -0
- package/dist/components/ui/loader.js +7 -0
- package/dist/components/ui/primitives/button.js +121 -0
- package/dist/components/ui/primitives/input.js +202 -0
- package/dist/components/ui/primitives/modal.js +203 -0
- package/dist/components/ui/primitives/text.js +286 -0
- package/dist/components/ui/resizeable.js +108 -0
- package/dist/components/ui/text.js +175 -0
- package/dist/components/ui/textarea.js +17 -0
- package/dist/components/ui/toast.js +129 -0
- package/dist/components/ui/view.js +250 -0
- package/dist/hooks/index.js +10 -0
- package/dist/hooks/useAvatars.js +32 -0
- package/dist/hooks/useCameraToggle.js +9 -0
- package/dist/hooks/useKeyboard.js +33 -0
- package/dist/hooks/useKeyboardSlide.js +11 -0
- package/dist/hooks/useLivestreamInfo.js +62 -0
- package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
- package/dist/hooks/usePlayerDimensions.js +19 -0
- package/dist/hooks/useSegmentDimensions.js +14 -0
- package/dist/hooks/useSegmentTiming.js +62 -0
- package/dist/index.js +10 -0
- package/dist/lib/facet.js +88 -0
- package/dist/lib/theme/atoms.js +620 -0
- package/dist/lib/theme/atoms.types.js +5 -0
- package/dist/lib/theme/index.js +9 -0
- package/dist/lib/theme/theme.js +248 -0
- package/dist/lib/theme/tokens.js +383 -0
- package/dist/lib/utils.js +94 -0
- package/dist/livestream-provider/index.js +8 -3
- package/dist/livestream-store/chat.js +89 -65
- package/dist/livestream-store/index.js +1 -0
- package/dist/livestream-store/livestream-store.js +3 -0
- package/dist/livestream-store/stream-key.js +115 -0
- package/dist/player-store/player-provider.js +0 -1
- package/dist/player-store/player-store.js +13 -0
- package/dist/streamplace-store/block.js +23 -0
- package/dist/streamplace-store/index.js +1 -0
- package/dist/streamplace-store/stream.js +193 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
- package/package.json +21 -5
- package/src/components/chat/chat-box.tsx +195 -0
- package/src/components/chat/chat-message.tsx +192 -0
- package/src/components/chat/chat.tsx +128 -0
- package/src/components/chat/mention-suggestions.tsx +71 -0
- package/src/components/chat/mod-view.tsx +118 -0
- package/src/components/mobile-player/fullscreen.native.tsx +193 -0
- package/src/components/mobile-player/fullscreen.tsx +79 -0
- package/src/components/mobile-player/player.tsx +134 -0
- package/src/components/mobile-player/props.tsx +11 -0
- package/src/components/mobile-player/shared.tsx +56 -0
- package/src/components/mobile-player/ui/countdown.tsx +119 -0
- package/src/components/mobile-player/ui/index.ts +6 -0
- package/src/components/mobile-player/ui/input.tsx +85 -0
- package/src/components/mobile-player/ui/metrics.tsx +69 -0
- package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +71 -0
- package/src/components/mobile-player/ui/viewers.tsx +32 -0
- package/src/components/mobile-player/use-webrtc.tsx +282 -0
- package/src/components/mobile-player/video.native.tsx +405 -0
- package/src/components/mobile-player/video.tsx +557 -0
- package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
- package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
- package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
- package/src/components/ui/button.tsx +309 -0
- package/src/components/ui/dialog.tsx +376 -0
- package/src/components/ui/dropdown.tsx +399 -0
- package/src/components/ui/icons.tsx +50 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +350 -0
- package/src/components/ui/loader.tsx +9 -0
- package/src/components/ui/primitives/button.tsx +292 -0
- package/src/components/ui/primitives/input.tsx +422 -0
- package/src/components/ui/primitives/modal.tsx +421 -0
- package/src/components/ui/primitives/text.tsx +499 -0
- package/src/components/ui/resizeable.tsx +181 -0
- package/src/components/ui/text.tsx +330 -0
- package/src/components/ui/textarea.tsx +34 -0
- package/src/components/ui/toast.tsx +203 -0
- package/src/components/ui/view.tsx +344 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useAvatars.tsx +44 -0
- package/src/hooks/useCameraToggle.ts +12 -0
- package/src/hooks/useKeyboard.tsx +41 -0
- package/src/hooks/useKeyboardSlide.ts +12 -0
- package/src/hooks/useLivestreamInfo.ts +67 -0
- package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
- package/src/hooks/usePlayerDimensions.ts +23 -0
- package/src/hooks/useSegmentDimensions.tsx +18 -0
- package/src/hooks/useSegmentTiming.tsx +88 -0
- package/src/index.tsx +21 -0
- package/src/lib/facet.ts +131 -0
- package/src/lib/theme/atoms.ts +760 -0
- package/src/lib/theme/atoms.types.ts +258 -0
- package/src/lib/theme/index.ts +48 -0
- package/src/lib/theme/theme.tsx +436 -0
- package/src/lib/theme/tokens.ts +409 -0
- package/src/lib/utils.ts +132 -0
- package/src/livestream-provider/index.tsx +13 -2
- package/src/livestream-store/chat.tsx +115 -78
- package/src/livestream-store/index.tsx +1 -0
- package/src/livestream-store/livestream-state.tsx +3 -0
- package/src/livestream-store/livestream-store.tsx +3 -0
- package/src/livestream-store/stream-key.tsx +124 -0
- package/src/player-store/player-provider.tsx +0 -1
- package/src/player-store/player-state.tsx +28 -0
- package/src/player-store/player-store.tsx +22 -0
- package/src/streamplace-store/block.tsx +29 -0
- package/src/streamplace-store/index.tsx +1 -0
- package/src/streamplace-store/stream.tsx +262 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { X } from "lucide-react-native";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Pressable } from "react-native";
|
|
5
|
+
import { Button, Loader, Text, useChat, useCreateChatMessage, useReplyToMessage, useSetReplyToMessage, View, } from "../../";
|
|
6
|
+
import { bg, flex, gap, h, layout, mb, pl, pr, w } from "../../lib/theme/atoms";
|
|
7
|
+
import { usePDSAgent } from "../../streamplace-store/xrpc";
|
|
8
|
+
import { Textarea } from "../ui/textarea";
|
|
9
|
+
import { RenderChatMessage } from "./chat-message";
|
|
10
|
+
import { MentionSuggestions } from "./mention-suggestions";
|
|
11
|
+
export function ChatBox({ isPopout, chatBoxStyle, }) {
|
|
12
|
+
const [submitting, setSubmitting] = useState(false);
|
|
13
|
+
const [message, setMessage] = useState("");
|
|
14
|
+
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
15
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
16
|
+
const [filteredAuthors, setFilteredAuthors] = useState(new Map());
|
|
17
|
+
const chat = useChat();
|
|
18
|
+
const createChatMessage = useCreateChatMessage();
|
|
19
|
+
const replyTo = useReplyToMessage();
|
|
20
|
+
const setReplyToMessage = useSetReplyToMessage();
|
|
21
|
+
const textAreaRef = useRef(null);
|
|
22
|
+
// are we logged in?
|
|
23
|
+
let agent = usePDSAgent();
|
|
24
|
+
if (!agent?.did) {
|
|
25
|
+
_jsx(View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[2]], children: _jsx(Text, { children: "Log in to chat." }) });
|
|
26
|
+
}
|
|
27
|
+
const authors = useMemo(() => {
|
|
28
|
+
if (!chat)
|
|
29
|
+
return null;
|
|
30
|
+
return chat.reduce((acc, msg) => {
|
|
31
|
+
acc.set(msg.author.handle, msg.chatProfile);
|
|
32
|
+
return acc;
|
|
33
|
+
}, new Map());
|
|
34
|
+
}, [chat]);
|
|
35
|
+
const handleMentionSelect = (handle) => {
|
|
36
|
+
const beforeAt = message.slice(0, message.lastIndexOf("@"));
|
|
37
|
+
setMessage(`${beforeAt}@${handle} `);
|
|
38
|
+
setShowSuggestions(false);
|
|
39
|
+
};
|
|
40
|
+
const updateSuggestions = (text) => {
|
|
41
|
+
const atIndex = text.lastIndexOf("@");
|
|
42
|
+
if (atIndex === -1 || !authors) {
|
|
43
|
+
setShowSuggestions(false);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const searchText = text.slice(atIndex + 1).toLowerCase();
|
|
47
|
+
const filteredAuthorsMap = new Map(Array.from(authors.entries()).filter(([handle]) => handle.toLowerCase().includes(searchText)));
|
|
48
|
+
setFilteredAuthors(filteredAuthorsMap);
|
|
49
|
+
setHighlightedIndex(0);
|
|
50
|
+
setShowSuggestions(filteredAuthorsMap.size > 0);
|
|
51
|
+
};
|
|
52
|
+
const submit = () => {
|
|
53
|
+
if (!message.trim())
|
|
54
|
+
return;
|
|
55
|
+
setMessage("");
|
|
56
|
+
setReplyToMessage(null);
|
|
57
|
+
setSubmitting(true);
|
|
58
|
+
createChatMessage({
|
|
59
|
+
text: message,
|
|
60
|
+
reply: replyTo || undefined,
|
|
61
|
+
});
|
|
62
|
+
setSubmitting(false);
|
|
63
|
+
};
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (replyTo && textAreaRef.current) {
|
|
66
|
+
textAreaRef.current.focus();
|
|
67
|
+
}
|
|
68
|
+
}, [replyTo]);
|
|
69
|
+
return (_jsxs(View, { style: [layout.flex.column, flex.shrink[1], gap.all[2]], children: [replyTo && (_jsxs(View, { style: [
|
|
70
|
+
layout.flex.row,
|
|
71
|
+
layout.flex.alignCenter,
|
|
72
|
+
layout.flex.spaceBetween,
|
|
73
|
+
h[12],
|
|
74
|
+
pl[2],
|
|
75
|
+
pr[10],
|
|
76
|
+
mb[2],
|
|
77
|
+
bg.gray[800],
|
|
78
|
+
{ borderRadius: 16 },
|
|
79
|
+
], children: [_jsx(RenderChatMessage, { item: replyTo, showReply: false, userCache: authors || new Map() }), _jsx(Pressable, { onPress: () => setReplyToMessage(null), children: _jsx(View, { style: [
|
|
80
|
+
layout.flex.row,
|
|
81
|
+
layout.flex.alignCenter,
|
|
82
|
+
layout.flex.justifyCenter,
|
|
83
|
+
h[12],
|
|
84
|
+
w[12],
|
|
85
|
+
bg.gray[600],
|
|
86
|
+
{ borderRadius: 999 },
|
|
87
|
+
], children: _jsx(X, { size: 24 }) }) })] })), _jsxs(View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[2]], children: [_jsx(Textarea, { ref: textAreaRef, numberOfLines: 1, value: message, enterKeyHint: "send", onSubmitEditing: submit, multiline: false, onChangeText: (text) => {
|
|
88
|
+
setMessage(text);
|
|
89
|
+
updateSuggestions(text);
|
|
90
|
+
}, onKeyPress: (k) => {
|
|
91
|
+
if (k.nativeEvent.key === "Enter") {
|
|
92
|
+
if (showSuggestions) {
|
|
93
|
+
k.preventDefault();
|
|
94
|
+
const handles = Array.from(filteredAuthors.keys());
|
|
95
|
+
if (handles.length > 0) {
|
|
96
|
+
handleMentionSelect(handles[highlightedIndex]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else
|
|
100
|
+
submit();
|
|
101
|
+
}
|
|
102
|
+
else if (k.nativeEvent.key === "ArrowUp") {
|
|
103
|
+
setHighlightedIndex((prev) => Math.max(prev - 1, 0));
|
|
104
|
+
}
|
|
105
|
+
else if (k.nativeEvent.key === "ArrowDown") {
|
|
106
|
+
setHighlightedIndex((prev) => Math.min(prev + 1, Array.from(filteredAuthors.keys()).length - 1));
|
|
107
|
+
}
|
|
108
|
+
}, style: [chatBoxStyle] }), _jsx(Button, { disabled: submitting, variant: "secondary", style: { borderRadius: 16, height: 36, minWidth: 80 }, onPress: submit, children: submitting ? _jsx(Loader, {}) : "Send" })] }), showSuggestions && (_jsx(MentionSuggestions, { authors: filteredAuthors || [], highlightedIndex: highlightedIndex, onSelect: handleMentionSelect }))] }));
|
|
109
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useCallback } from "react";
|
|
3
|
+
import { Linking, View } from "react-native";
|
|
4
|
+
import { segmentize } from "../../lib/facet";
|
|
5
|
+
import { borders, flex, gap, ml, mr, opacity, pl, w, } from "../../lib/theme/atoms";
|
|
6
|
+
import { atoms, layout } from "../ui";
|
|
7
|
+
import { useLivestreamStore } from "../../livestream-store";
|
|
8
|
+
import { Text } from "../ui/text";
|
|
9
|
+
const getRgbColor = (color) => color ? `rgb(${color.red}, ${color.green}, ${color.blue})` : "$accentColor";
|
|
10
|
+
const segmentedObject = (obj, index, userCache) => {
|
|
11
|
+
if (obj.features && obj.features.length > 0) {
|
|
12
|
+
let ftr = obj.features[0];
|
|
13
|
+
// afaik there shouldn't be a case where facets overlap, at least currently
|
|
14
|
+
if (ftr.$type === "app.bsky.richtext.facet#link") {
|
|
15
|
+
let linkftr = ftr;
|
|
16
|
+
return (_jsx(Text, { style: [{ color: atoms.colors.ios.systemBlue, cursor: "pointer" }], onPress: () => Linking.openURL(linkftr.uri || ""), children: obj.text }, `mention-${index}`));
|
|
17
|
+
}
|
|
18
|
+
else if (ftr.$type === "app.bsky.richtext.facet#mention") {
|
|
19
|
+
let mtnftr = ftr;
|
|
20
|
+
const profile = userCache?.[mtnftr.did];
|
|
21
|
+
console.log(profile, mtnftr.did, userCache);
|
|
22
|
+
return (_jsx(Text, { style: [
|
|
23
|
+
{
|
|
24
|
+
cursor: "pointer",
|
|
25
|
+
color: getRgbColor(profile?.color),
|
|
26
|
+
},
|
|
27
|
+
], onPress: () => Linking.openURL(`https://bsky.app/profile/${mtnftr.did || ""}`), children: obj.text }, `mention-${index}`));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
return _jsx(Text, { children: obj.text }, `text-${index}`);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const RichTextMessage = ({ text, facets, }) => {
|
|
35
|
+
if (!facets?.length)
|
|
36
|
+
return _jsx(Text, { children: text });
|
|
37
|
+
const userCache = useLivestreamStore((state) => state.authors);
|
|
38
|
+
let segs = segmentize(text, facets);
|
|
39
|
+
return segs.map((seg, i) => segmentedObject(seg, i, userCache));
|
|
40
|
+
};
|
|
41
|
+
export const RenderChatMessage = memo(function RenderChatMessage({ item, showReply = true, showTime = true, }) {
|
|
42
|
+
const formatTime = useCallback((dateString) => {
|
|
43
|
+
return new Date(dateString).toLocaleString(undefined, {
|
|
44
|
+
hour: "2-digit",
|
|
45
|
+
minute: "2-digit",
|
|
46
|
+
hour12: false,
|
|
47
|
+
});
|
|
48
|
+
}, []);
|
|
49
|
+
return (_jsxs(_Fragment, { children: [item.replyTo && showReply && (_jsx(View, { style: [
|
|
50
|
+
gap.all[2],
|
|
51
|
+
layout.flex.row,
|
|
52
|
+
w.percent[100],
|
|
53
|
+
borders.left.width.medium,
|
|
54
|
+
borders.left.color.gray[700],
|
|
55
|
+
ml[4],
|
|
56
|
+
pl[4],
|
|
57
|
+
opacity[80],
|
|
58
|
+
], children: _jsxs(Text, { numberOfLines: 1, style: [flex.shrink[1], mr[4]], children: [_jsxs(Text, { style: {
|
|
59
|
+
color: getRgbColor(item.replyTo.chatProfile.color),
|
|
60
|
+
fontWeight: "thin",
|
|
61
|
+
}, children: ["@", item.replyTo.author.handle] }), " ", _jsx(Text, { style: {
|
|
62
|
+
color: atoms.colors.gray[300],
|
|
63
|
+
fontStyle: "italic",
|
|
64
|
+
}, children: item.replyTo.record.text })] }) })), _jsxs(View, { style: [gap.all[2], layout.flex.row, w.percent[100]], children: [showTime && (_jsx(Text, { style: {
|
|
65
|
+
fontVariant: ["tabular-nums"],
|
|
66
|
+
color: atoms.colors.gray[300],
|
|
67
|
+
}, children: formatTime(item.record.createdAt) })), _jsxs(Text, { weight: "bold", color: "default", style: [flex.shrink[1]], children: [_jsxs(Text, { style: [
|
|
68
|
+
{
|
|
69
|
+
cursor: "pointer",
|
|
70
|
+
color: getRgbColor(item.chatProfile?.color),
|
|
71
|
+
},
|
|
72
|
+
], children: ["@", item.author.handle] }), ":", " ", _jsx(RichTextMessage, { text: item.record.text, facets: item.record.facets || [] })] })] })] }));
|
|
73
|
+
}, (prevProps, nextProps) => {
|
|
74
|
+
return (prevProps.item.author.handle === nextProps.item.author.handle &&
|
|
75
|
+
prevProps.item.record.text === nextProps.item.record.text);
|
|
76
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Reply, ShieldEllipsis } from "lucide-react-native";
|
|
3
|
+
import { memo, useRef } from "react";
|
|
4
|
+
import { FlatList, Platform, Pressable } from "react-native";
|
|
5
|
+
import Swipeable from "react-native-gesture-handler/ReanimatedSwipeable";
|
|
6
|
+
import Reanimated, { useAnimatedStyle, } from "react-native-reanimated";
|
|
7
|
+
import { Text, useChat, usePlayerStore, useSetReplyToMessage, View, } from "../../";
|
|
8
|
+
import { flex, py, w } from "../../lib/theme/atoms";
|
|
9
|
+
import { RenderChatMessage } from "./chat-message";
|
|
10
|
+
import { ModView } from "./mod-view";
|
|
11
|
+
function RightAction(prog, drag) {
|
|
12
|
+
const styleAnimation = useAnimatedStyle(() => {
|
|
13
|
+
return {
|
|
14
|
+
transform: [{ translateX: drag.value + 25 }],
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
return (_jsx(Reanimated.View, { style: [styleAnimation], children: _jsx(Reply, { color: "white" }) }));
|
|
18
|
+
}
|
|
19
|
+
function LeftAction(prog, drag) {
|
|
20
|
+
const styleAnimation = useAnimatedStyle(() => {
|
|
21
|
+
return {
|
|
22
|
+
transform: [{ translateX: drag.value - 25 }],
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
return (_jsx(Reanimated.View, { style: [styleAnimation], children: _jsx(ShieldEllipsis, { color: "white" }) }));
|
|
26
|
+
}
|
|
27
|
+
const SHOWN_MSGS = Platform.OS === "android" || Platform.OS === "ios" ? 100 : 25;
|
|
28
|
+
const keyExtractor = (item, index) => {
|
|
29
|
+
return `${item.uri}`;
|
|
30
|
+
};
|
|
31
|
+
const ChatLine = memo(({ item }) => {
|
|
32
|
+
const setReply = useSetReplyToMessage();
|
|
33
|
+
const setModMsg = usePlayerStore((state) => state.setModMessage);
|
|
34
|
+
const swipeableRef = useRef(null);
|
|
35
|
+
return (_jsx(Pressable, { onLongPress: () => setModMsg(item), children: _jsx(Swipeable, { containerStyle: [py[1]], friction: 2, enableTrackpadTwoFingerGesture: true, rightThreshold: 40, renderRightActions: Platform.OS === "android" ? undefined : RightAction, renderLeftActions: Platform.OS === "android" ? undefined : LeftAction, overshootFriction: 9, ref: (ref) => {
|
|
36
|
+
swipeableRef.current = ref;
|
|
37
|
+
}, onSwipeableOpen: (r) => {
|
|
38
|
+
if (r === (Platform.OS === "android" ? "right" : "left")) {
|
|
39
|
+
setReply(item);
|
|
40
|
+
}
|
|
41
|
+
if (r === (Platform.OS === "android" ? "left" : "right")) {
|
|
42
|
+
setModMsg(item);
|
|
43
|
+
}
|
|
44
|
+
// close this swipeable
|
|
45
|
+
const swipeable = swipeableRef.current;
|
|
46
|
+
if (swipeable) {
|
|
47
|
+
swipeable.close();
|
|
48
|
+
}
|
|
49
|
+
}, children: _jsx(RenderChatMessage, { item: item }) }) }));
|
|
50
|
+
});
|
|
51
|
+
export function Chat({ shownMessages = SHOWN_MSGS, style: propsStyle, ...props }) {
|
|
52
|
+
const chat = useChat();
|
|
53
|
+
if (!chat)
|
|
54
|
+
return (_jsx(View, { style: [flex.shrink[1]], children: _jsx(Text, { children: "Loading chaat..." }) }));
|
|
55
|
+
return (_jsxs(View, { style: [flex.shrink[1]].concat(propsStyle || []), children: [_jsx(FlatList, { style: [flex.grow[1], flex.shrink[1], w.percent[100]], data: chat, inverted: true, keyExtractor: keyExtractor, renderItem: ({ item, index }) => _jsx(ChatLine, { item: item }), removeClippedSubviews: true, maxToRenderPerBatch: 10, initialNumToRender: 10, updateCellsBatchingPeriod: 50 }), _jsx(ModView, {})] }));
|
|
56
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Pressable } from "react-native";
|
|
3
|
+
import { Text, View } from "../..";
|
|
4
|
+
import { bg, layout, left, right, zIndex } from "../../lib/theme/atoms";
|
|
5
|
+
export function MentionSuggestions({ authors, onSelect, highlightedIndex, }) {
|
|
6
|
+
if (!authors || authors.size === 0) {
|
|
7
|
+
return null; // No authors to display
|
|
8
|
+
}
|
|
9
|
+
const authorHandles = Array.from(authors.keys());
|
|
10
|
+
return (_jsx(View, { style: [
|
|
11
|
+
bg.gray[800],
|
|
12
|
+
layout.position.absolute,
|
|
13
|
+
left[0],
|
|
14
|
+
right[0],
|
|
15
|
+
zIndex[50],
|
|
16
|
+
{
|
|
17
|
+
bottom: "100%",
|
|
18
|
+
borderRadius: 8,
|
|
19
|
+
boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
|
|
20
|
+
},
|
|
21
|
+
], children: authorHandles.map((handle, index) => {
|
|
22
|
+
let profile = authors.get(handle);
|
|
23
|
+
return (_jsx(Pressable, { onPress: () => onSelect(handle), style: [
|
|
24
|
+
{
|
|
25
|
+
padding: 8,
|
|
26
|
+
flexDirection: "row",
|
|
27
|
+
alignItems: "center",
|
|
28
|
+
backgroundColor: index === highlightedIndex
|
|
29
|
+
? "rgba(0, 0, 0, 0.1)"
|
|
30
|
+
: "rgba(0, 0, 0, 0.5)",
|
|
31
|
+
},
|
|
32
|
+
], children: _jsxs(Text, { style: {
|
|
33
|
+
color: profile?.color
|
|
34
|
+
? `rgb(${profile.color.red}, ${profile.color.green}, ${profile.color.blue})`
|
|
35
|
+
: "black",
|
|
36
|
+
fontWeight: "bold",
|
|
37
|
+
}, children: ["@", handle] }) }, handle));
|
|
38
|
+
}) }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useRef } from "react";
|
|
3
|
+
import { gap, mr } from "../../lib/theme/atoms";
|
|
4
|
+
import { usePlayerStore } from "../../player-store";
|
|
5
|
+
import { useCreateBlockRecord } from "../../streamplace-store/block";
|
|
6
|
+
import { usePDSAgent } from "../../streamplace-store/xrpc";
|
|
7
|
+
import { DropdownMenu, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger, layout, ResponsiveDropdownMenuContent, Text, View, } from "../ui";
|
|
8
|
+
import { RenderChatMessage } from "./chat-message";
|
|
9
|
+
export const ModView = forwardRef(() => {
|
|
10
|
+
const triggerRef = useRef(null);
|
|
11
|
+
const message = usePlayerStore((state) => state.modMessage);
|
|
12
|
+
let agent = usePDSAgent();
|
|
13
|
+
let createBlockRecord = useCreateBlockRecord();
|
|
14
|
+
if (!agent?.did) {
|
|
15
|
+
_jsx(View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[2]], children: _jsx(Text, { children: "Log in to submit mod actions" }) });
|
|
16
|
+
}
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (message) {
|
|
19
|
+
console.log("opening mod view");
|
|
20
|
+
triggerRef.current?.open();
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.log("closing mod view");
|
|
24
|
+
triggerRef.current?.close();
|
|
25
|
+
}
|
|
26
|
+
}, [message]);
|
|
27
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { ref: triggerRef, children: _jsx(View, {}) }), _jsx(ResponsiveDropdownMenuContent, { children: message && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuGroup, { children: _jsx(DropdownMenuItem, { children: _jsx(View, { style: [layout.flex.column, mr[5], { gap: 6 }], children: _jsx(RenderChatMessage, { item: message }) }) }) }), _jsx(DropdownMenuGroup, { title: `Moderation actions`, children: _jsx(DropdownMenuItem, { disabled: message.author.did === agent?.did, onPress: () => {
|
|
28
|
+
console.log("Creating block record");
|
|
29
|
+
createBlockRecord(message.author.did)
|
|
30
|
+
.then((r) => console.log(r))
|
|
31
|
+
.catch((e) => console.error(e));
|
|
32
|
+
}, children: _jsx(Text, { color: "destructive", children: message.author.did === agent?.did ? (_jsx(_Fragment, { children: "Block yourself (you can't block yourself)" })) : (_jsxs(_Fragment, { children: ["Block user @", message.author.handle, " from this channel"] })) }) }) })] })) })] }));
|
|
33
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { getFirstPlayerID, usePlayerStore } from "../..";
|
|
4
|
+
import { View } from "../../components/ui";
|
|
5
|
+
import Video from "./video";
|
|
6
|
+
export function Fullscreen(props) {
|
|
7
|
+
const playerId = getFirstPlayerID();
|
|
8
|
+
const protocol = usePlayerStore((x) => x.protocol, playerId);
|
|
9
|
+
const fullscreen = usePlayerStore((x) => x.fullscreen, playerId);
|
|
10
|
+
const setFullscreen = usePlayerStore((x) => x.setFullscreen, playerId);
|
|
11
|
+
const setSrc = usePlayerStore((x) => x.setSrc);
|
|
12
|
+
const divRef = useRef(null);
|
|
13
|
+
const videoRef = useRef(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
setSrc(props.src);
|
|
16
|
+
}, [props.src]);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!divRef.current) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
(async () => {
|
|
22
|
+
if (fullscreen && !document.fullscreenElement) {
|
|
23
|
+
try {
|
|
24
|
+
const div = divRef.current;
|
|
25
|
+
if (typeof div.requestFullscreen === "function") {
|
|
26
|
+
await div.requestFullscreen();
|
|
27
|
+
}
|
|
28
|
+
else if (videoRef.current) {
|
|
29
|
+
if (typeof videoRef.current.webkitEnterFullscreen ===
|
|
30
|
+
"function") {
|
|
31
|
+
await videoRef.current.webkitEnterFullscreen();
|
|
32
|
+
}
|
|
33
|
+
else if (typeof videoRef.current.requestFullscreen === "function") {
|
|
34
|
+
await videoRef.current.requestFullscreen();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
setFullscreen(true);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
console.error("fullscreen failed", e.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!fullscreen) {
|
|
44
|
+
if (document.fullscreenElement) {
|
|
45
|
+
try {
|
|
46
|
+
await document.exitFullscreen();
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
console.error("fullscreen exit failed", e.message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
setFullscreen(false);
|
|
53
|
+
}
|
|
54
|
+
})();
|
|
55
|
+
}, [fullscreen, protocol]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const listener = () => {
|
|
58
|
+
console.log("fullscreenchange", document.fullscreenElement);
|
|
59
|
+
setFullscreen(!!document.fullscreenElement);
|
|
60
|
+
};
|
|
61
|
+
document.body.addEventListener("fullscreenchange", listener);
|
|
62
|
+
document.body.addEventListener("webkitfullscreenchange", listener);
|
|
63
|
+
return () => {
|
|
64
|
+
document.body.removeEventListener("fullscreenchange", listener);
|
|
65
|
+
document.body.removeEventListener("webkitfullscreenchange", listener);
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
return (_jsx(View, { ref: divRef, children: _jsx(Video, {}) }));
|
|
69
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useNavigation } from "@react-navigation/native";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { BackHandler, Dimensions, StyleSheet, View } from "react-native";
|
|
5
|
+
import { SystemBars } from "react-native-edge-to-edge";
|
|
6
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
7
|
+
import { PlayerProtocol, useLivestreamStore, usePlayerStore } from "../..";
|
|
8
|
+
import Video from "./video.native";
|
|
9
|
+
// Standard 16:9 video aspect ratio
|
|
10
|
+
const VIDEO_ASPECT_RATIO = 16 / 9;
|
|
11
|
+
export function Fullscreen(props) {
|
|
12
|
+
const ref = useRef(null);
|
|
13
|
+
const insets = useSafeAreaInsets();
|
|
14
|
+
const navigation = useNavigation();
|
|
15
|
+
const [dimensions, setDimensions] = useState(Dimensions.get("window"));
|
|
16
|
+
// Get state from player store
|
|
17
|
+
const protocol = usePlayerStore((x) => x.protocol);
|
|
18
|
+
const fullscreen = usePlayerStore((x) => x.fullscreen);
|
|
19
|
+
const setFullscreen = usePlayerStore((x) => x.setFullscreen);
|
|
20
|
+
const handle = useLivestreamStore((x) => x.profile?.handle);
|
|
21
|
+
const setSrc = usePlayerStore((x) => x.setSrc);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setSrc(props.src);
|
|
24
|
+
}, [props.src]);
|
|
25
|
+
// Re-calculate dimensions on orientation change
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const updateDimensions = () => {
|
|
28
|
+
setDimensions(Dimensions.get("window"));
|
|
29
|
+
};
|
|
30
|
+
const subscription = Dimensions.addEventListener("change", updateDimensions);
|
|
31
|
+
return () => {
|
|
32
|
+
subscription.remove();
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
35
|
+
// Hide status bar when in fullscreen mode
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (fullscreen) {
|
|
38
|
+
SystemBars.setHidden(true);
|
|
39
|
+
console.log("setting sidebar hidden");
|
|
40
|
+
// Hide the navigation header
|
|
41
|
+
navigation.setOptions({
|
|
42
|
+
headerShown: false,
|
|
43
|
+
});
|
|
44
|
+
// Handle hardware back button
|
|
45
|
+
const backHandler = BackHandler.addEventListener("hardwareBackPress", () => {
|
|
46
|
+
setFullscreen(false);
|
|
47
|
+
return true;
|
|
48
|
+
});
|
|
49
|
+
return () => {
|
|
50
|
+
backHandler.remove();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
SystemBars.setHidden(false);
|
|
55
|
+
// Restore the navigation header
|
|
56
|
+
navigation.setOptions({
|
|
57
|
+
headerShown: true,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return () => {
|
|
61
|
+
SystemBars.setHidden(false);
|
|
62
|
+
// Ensure header is restored if component unmounts
|
|
63
|
+
navigation.setOptions({
|
|
64
|
+
headerShown: true,
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
}, [fullscreen, navigation, setFullscreen]);
|
|
68
|
+
// Handle fullscreen state changes for native video players
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
// For WebRTC, we handle fullscreen manually via the custom implementation
|
|
71
|
+
if (protocol === PlayerProtocol.WEBRTC) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// For HLS and other protocols, sync with native fullscreen
|
|
75
|
+
if (ref.current) {
|
|
76
|
+
if (fullscreen) {
|
|
77
|
+
ref.current.enterFullscreen();
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
ref.current.exitFullscreen();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}, [fullscreen, protocol]);
|
|
84
|
+
if (fullscreen && protocol === PlayerProtocol.WEBRTC) {
|
|
85
|
+
// Determine if we're in landscape mode
|
|
86
|
+
const isLandscape = dimensions.width > dimensions.height;
|
|
87
|
+
// Calculate video container dimensions based on screen size and orientation
|
|
88
|
+
let videoWidth;
|
|
89
|
+
let videoHeight;
|
|
90
|
+
if (isLandscape) {
|
|
91
|
+
// In landscape, account for safe areas and use available height
|
|
92
|
+
const availableHeight = dimensions.height - (insets.top + insets.bottom);
|
|
93
|
+
const availableWidth = dimensions.width - (insets.left + insets.right);
|
|
94
|
+
videoHeight = availableHeight;
|
|
95
|
+
videoWidth = videoHeight * VIDEO_ASPECT_RATIO;
|
|
96
|
+
// If calculated width exceeds available width, constrain and maintain aspect ratio
|
|
97
|
+
if (videoWidth > availableWidth) {
|
|
98
|
+
videoWidth = availableWidth;
|
|
99
|
+
videoHeight = videoWidth / VIDEO_ASPECT_RATIO;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// In portrait, account for safe areas
|
|
104
|
+
const availableWidth = dimensions.width - (insets.left + insets.right);
|
|
105
|
+
videoWidth = availableWidth;
|
|
106
|
+
videoHeight = videoWidth / VIDEO_ASPECT_RATIO;
|
|
107
|
+
}
|
|
108
|
+
// Calculate position to center the video, accounting for safe areas
|
|
109
|
+
const leftPosition = (dimensions.width - videoWidth) / 2;
|
|
110
|
+
const topPosition = (dimensions.height - videoHeight) / 2;
|
|
111
|
+
// When in custom fullscreen mode
|
|
112
|
+
return (_jsx(View, { style: [
|
|
113
|
+
styles.fullscreenContainer,
|
|
114
|
+
{
|
|
115
|
+
width: isLandscape ? dimensions.width + 40 : dimensions.width,
|
|
116
|
+
height: dimensions.height,
|
|
117
|
+
},
|
|
118
|
+
], children: _jsx(View, { style: [
|
|
119
|
+
styles.videoContainer,
|
|
120
|
+
{
|
|
121
|
+
width: isLandscape ? videoWidth + 40 : videoWidth,
|
|
122
|
+
height: videoHeight,
|
|
123
|
+
left: leftPosition,
|
|
124
|
+
top: topPosition,
|
|
125
|
+
},
|
|
126
|
+
], children: _jsx(Video, {}) }) }));
|
|
127
|
+
}
|
|
128
|
+
// Normal non-fullscreen mode
|
|
129
|
+
return (_jsx(_Fragment, { children: _jsx(Video, {}) }));
|
|
130
|
+
}
|
|
131
|
+
const styles = StyleSheet.create({
|
|
132
|
+
fullscreenContainer: {
|
|
133
|
+
position: "absolute",
|
|
134
|
+
top: 0,
|
|
135
|
+
left: 0,
|
|
136
|
+
right: 0,
|
|
137
|
+
bottom: 0,
|
|
138
|
+
backgroundColor: "#000",
|
|
139
|
+
zIndex: 9999,
|
|
140
|
+
elevation: 9999,
|
|
141
|
+
margin: 0,
|
|
142
|
+
padding: 0,
|
|
143
|
+
justifyContent: "center",
|
|
144
|
+
alignItems: "center",
|
|
145
|
+
},
|
|
146
|
+
videoContainer: {
|
|
147
|
+
position: "absolute",
|
|
148
|
+
backgroundColor: "#111",
|
|
149
|
+
overflow: "hidden",
|
|
150
|
+
},
|
|
151
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { flex, layout, w, zIndex } from "../../lib/theme/atoms";
|
|
4
|
+
import { useSegment } from "../../livestream-store";
|
|
5
|
+
import { PlayerStatus, usePlayerStore, } from "../../player-store";
|
|
6
|
+
import { useStreamplaceStore } from "../../streamplace-store";
|
|
7
|
+
import { Text, View } from "../ui";
|
|
8
|
+
import { Fullscreen } from "./fullscreen";
|
|
9
|
+
const OFFLINE_THRESHOLD = 10000;
|
|
10
|
+
export * as PlayerUI from "./ui";
|
|
11
|
+
export function Player(props) {
|
|
12
|
+
const playing = usePlayerStore((x) => x.status === PlayerStatus.PLAYING);
|
|
13
|
+
const setOffline = usePlayerStore((x) => x.setOffline);
|
|
14
|
+
const setIngest = usePlayerStore((x) => x.setIngestConnectionState);
|
|
15
|
+
const clearControlsTimeout = usePlayerStore((x) => x.clearControlsTimeout);
|
|
16
|
+
// Will call back every few seconds to send health updates
|
|
17
|
+
usePlayerStatus();
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
setIngest(props.ingest ? "new" : null);
|
|
20
|
+
}, []);
|
|
21
|
+
if (typeof props.src !== "string") {
|
|
22
|
+
return (_jsx(View, { children: _jsx(Text, { children: "No source provided \uD83E\uDD37" }) }));
|
|
23
|
+
}
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
return () => {
|
|
26
|
+
clearControlsTimeout();
|
|
27
|
+
};
|
|
28
|
+
}, []);
|
|
29
|
+
const segment = useSegment();
|
|
30
|
+
const [lastCheck, setLastCheck] = useState(0);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (playing) {
|
|
33
|
+
setOffline(false);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!segment) {
|
|
37
|
+
setOffline(false);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const startTime = Date.parse(segment.startTime);
|
|
41
|
+
if (!startTime) {
|
|
42
|
+
console.error("startTime is not a number", segment.startTime);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const timeSinceStart = Date.now() - startTime;
|
|
46
|
+
if (timeSinceStart > OFFLINE_THRESHOLD) {
|
|
47
|
+
setOffline(true);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const handle = setTimeout(() => {
|
|
51
|
+
setLastCheck(Date.now());
|
|
52
|
+
}, 1000);
|
|
53
|
+
return () => clearTimeout(handle);
|
|
54
|
+
}, [segment, playing, lastCheck]);
|
|
55
|
+
return (_jsx(_Fragment, { children: _jsx(View, { style: [zIndex[0], flex.values[1], w.percent[100], layout.flex.center], children: _jsx(Fullscreen, { src: props.src }) }) }));
|
|
56
|
+
}
|
|
57
|
+
const POLL_INTERVAL = 5000;
|
|
58
|
+
export function usePlayerStatus() {
|
|
59
|
+
const playerStatus = usePlayerStore((x) => x.status);
|
|
60
|
+
const url = useStreamplaceStore((x) => x.url);
|
|
61
|
+
const playerEvent = usePlayerStore((x) => x.playerEvent);
|
|
62
|
+
const [whatDoing, setWhatDoing] = useState(PlayerStatus.START);
|
|
63
|
+
const [whatDid, setWhatDid] = useState({});
|
|
64
|
+
const [doingSince, setDoingSince] = useState(Date.now());
|
|
65
|
+
const [lastUpdated, setLastUpdated] = useState(0);
|
|
66
|
+
const updateWhatDid = (now) => {
|
|
67
|
+
const prev = whatDid[whatDoing] ?? 0;
|
|
68
|
+
const duration = now.getTime() - doingSince;
|
|
69
|
+
const ret = {
|
|
70
|
+
...whatDid,
|
|
71
|
+
[whatDoing]: prev + duration,
|
|
72
|
+
};
|
|
73
|
+
return ret;
|
|
74
|
+
};
|
|
75
|
+
// callback to update the status
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const now = new Date();
|
|
78
|
+
if (playerStatus !== whatDoing) {
|
|
79
|
+
setWhatDid(updateWhatDid(now));
|
|
80
|
+
setWhatDoing(playerStatus);
|
|
81
|
+
setDoingSince(now.getTime());
|
|
82
|
+
}
|
|
83
|
+
}, [playerStatus]);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (lastUpdated === 0) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const now = new Date();
|
|
89
|
+
const fullWhatDid = updateWhatDid(now);
|
|
90
|
+
setWhatDid({});
|
|
91
|
+
setDoingSince(now.getTime());
|
|
92
|
+
playerEvent(url, now.toISOString(), "aq-played", {
|
|
93
|
+
whatHappened: fullWhatDid,
|
|
94
|
+
});
|
|
95
|
+
}, [lastUpdated]);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const interval = setInterval((_) => {
|
|
98
|
+
setLastUpdated(Date.now());
|
|
99
|
+
}, POLL_INTERVAL);
|
|
100
|
+
return () => clearInterval(interval);
|
|
101
|
+
}, []);
|
|
102
|
+
return [whatDoing];
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|