@streamplace/components 0.7.18 → 0.7.21
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/assets/emoji-data.json +19371 -0
- package/dist/components/chat/chat-box.js +319 -0
- package/dist/components/chat/chat-message.js +87 -0
- package/dist/components/chat/chat.js +150 -0
- package/dist/components/chat/emoji-suggestions.js +35 -0
- package/dist/components/chat/mention-suggestions.js +42 -0
- package/dist/components/chat/mod-view.js +112 -0
- package/dist/components/chat/system-message.js +19 -0
- package/dist/components/dashboard/chat-panel.js +38 -0
- package/dist/components/dashboard/header.js +80 -0
- package/dist/components/dashboard/index.js +14 -0
- package/dist/components/dashboard/information-widget.js +234 -0
- package/dist/components/dashboard/mod-actions.js +71 -0
- package/dist/components/dashboard/problems.js +74 -0
- package/dist/components/icons/bluesky-icon.js +9 -0
- package/dist/components/keep-awake.js +7 -0
- package/dist/components/keep-awake.native.js +16 -0
- package/dist/components/mobile-player/fullscreen.js +76 -0
- package/dist/components/mobile-player/fullscreen.native.js +141 -0
- package/dist/components/mobile-player/player.js +94 -0
- package/dist/components/mobile-player/props.js +2 -0
- package/dist/components/mobile-player/shared.js +54 -0
- package/dist/components/mobile-player/ui/autoplay-button.js +68 -0
- package/dist/components/mobile-player/ui/countdown.js +83 -0
- package/dist/components/mobile-player/ui/index.js +12 -0
- package/dist/components/mobile-player/ui/input.js +42 -0
- package/dist/components/mobile-player/ui/metrics.js +44 -0
- package/dist/components/mobile-player/ui/report-modal.js +90 -0
- package/dist/components/mobile-player/ui/streamer-context-menu.js +7 -0
- package/dist/components/mobile-player/ui/streamer-loading-overlay.js +104 -0
- package/dist/components/mobile-player/ui/viewer-context-menu.js +51 -0
- package/dist/components/mobile-player/ui/viewer-loading-overlay.js +49 -0
- package/dist/components/mobile-player/ui/viewers.js +23 -0
- package/dist/components/mobile-player/use-webrtc.js +243 -0
- package/dist/components/mobile-player/video-async.native.js +276 -0
- package/dist/components/mobile-player/video-retry.js +29 -0
- package/dist/components/mobile-player/video.js +475 -0
- package/dist/components/mobile-player/video.native.js +56 -0
- package/dist/components/mobile-player/webrtc-diagnostics.js +110 -0
- package/dist/components/mobile-player/webrtc-primitives.js +27 -0
- package/dist/components/mobile-player/webrtc-primitives.native.js +8 -0
- package/dist/components/share/sharesheet.js +91 -0
- package/dist/components/ui/button.js +223 -0
- package/dist/components/ui/dialog.js +206 -0
- package/dist/components/ui/dropdown.js +172 -0
- package/dist/components/ui/icons.js +25 -0
- package/dist/components/ui/index.js +34 -0
- package/dist/components/ui/info-box.js +31 -0
- package/dist/components/ui/info-row.js +23 -0
- package/dist/components/ui/input.js +205 -0
- package/dist/components/ui/loader.js +10 -0
- package/dist/components/ui/primitives/button.js +125 -0
- package/dist/components/ui/primitives/input.js +206 -0
- package/dist/components/ui/primitives/modal.js +206 -0
- package/dist/components/ui/primitives/text.js +292 -0
- package/dist/components/ui/resizeable.js +121 -0
- package/dist/components/ui/slider.js +5 -0
- package/dist/components/ui/text.js +177 -0
- package/dist/components/ui/textarea.js +19 -0
- package/dist/components/ui/toast.js +175 -0
- package/dist/components/ui/view.js +252 -0
- package/dist/hooks/index.js +14 -0
- package/dist/hooks/useAvatars.js +35 -0
- package/dist/hooks/useCameraToggle.js +12 -0
- package/dist/hooks/useKeyboard.js +36 -0
- package/dist/hooks/useKeyboardSlide.js +14 -0
- package/dist/hooks/useLivestreamInfo.js +69 -0
- package/dist/hooks/useOuterAndInnerDimensions.js +30 -0
- package/dist/hooks/usePlayerDimensions.js +22 -0
- package/dist/hooks/usePointerDevice.js +71 -0
- package/dist/hooks/useSegmentDimensions.js +17 -0
- package/dist/hooks/useSegmentTiming.js +65 -0
- package/dist/index.js +34 -0
- package/dist/lib/browser.js +35 -0
- package/dist/lib/facet.js +92 -0
- package/dist/lib/system-messages.js +101 -0
- package/dist/lib/theme/atoms.js +646 -0
- package/dist/lib/theme/atoms.types.js +6 -0
- package/dist/lib/theme/index.js +35 -0
- package/dist/lib/theme/theme.js +256 -0
- package/dist/lib/theme/tokens.js +659 -0
- package/dist/lib/utils.js +105 -0
- package/dist/livestream-provider/index.js +30 -0
- package/dist/livestream-provider/websocket.js +45 -0
- package/dist/livestream-store/chat.js +308 -0
- package/dist/livestream-store/context.js +5 -0
- package/dist/livestream-store/index.js +7 -0
- package/dist/livestream-store/livestream-state.js +2 -0
- package/dist/livestream-store/livestream-store.js +58 -0
- package/dist/livestream-store/problems.js +76 -0
- package/dist/livestream-store/stream-key.js +88 -0
- package/dist/livestream-store/websocket-consumer.js +94 -0
- package/dist/player-store/context.js +5 -0
- package/dist/player-store/index.js +9 -0
- package/dist/player-store/player-provider.js +58 -0
- package/dist/player-store/player-state.js +25 -0
- package/dist/player-store/player-store.js +201 -0
- package/dist/player-store/single-player-provider.js +121 -0
- package/dist/streamplace-provider/context.js +5 -0
- package/dist/streamplace-provider/index.js +20 -0
- package/dist/streamplace-provider/poller.js +49 -0
- package/dist/streamplace-provider/xrpc.js +0 -0
- package/dist/streamplace-store/block.js +65 -0
- package/dist/streamplace-store/index.js +6 -0
- package/dist/streamplace-store/stream.js +247 -0
- package/dist/streamplace-store/streamplace-store.js +47 -0
- package/dist/streamplace-store/user.js +52 -0
- package/dist/streamplace-store/xrpc.js +15 -0
- package/dist/ui/index.js +79 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/package.json +5 -4
- package/src/components/chat/chat-box.tsx +3 -0
- package/src/components/chat/mod-view.tsx +39 -5
- package/src/components/mobile-player/fullscreen.tsx +2 -0
- package/src/components/mobile-player/ui/autoplay-button.tsx +86 -0
- package/src/components/mobile-player/ui/index.ts +1 -0
- package/src/components/mobile-player/video.tsx +11 -1
- package/src/livestream-store/chat.tsx +22 -0
- package/src/player-store/player-provider.tsx +2 -1
- package/src/player-store/player-state.tsx +6 -0
- package/src/player-store/player-store.tsx +4 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mergeStyles = mergeStyles;
|
|
4
|
+
exports.createStyleMerger = createStyleMerger;
|
|
5
|
+
exports.conditionalStyle = conditionalStyle;
|
|
6
|
+
exports.responsiveValue = responsiveValue;
|
|
7
|
+
exports.platformStyle = platformStyle;
|
|
8
|
+
exports.hexToRgba = hexToRgba;
|
|
9
|
+
exports.debounce = debounce;
|
|
10
|
+
exports.throttle = throttle;
|
|
11
|
+
exports.forwardProps = forwardProps;
|
|
12
|
+
const react_native_1 = require("react-native");
|
|
13
|
+
/**
|
|
14
|
+
* Merges React Native styles similar to how cn() merges CSS classes
|
|
15
|
+
* Handles arrays, objects, and falsy values
|
|
16
|
+
*/
|
|
17
|
+
function mergeStyles(...styles) {
|
|
18
|
+
const validStyles = styles.filter(Boolean).flat();
|
|
19
|
+
return react_native_1.StyleSheet.flatten(validStyles) || {};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Creates a style merger function that includes base styles
|
|
23
|
+
* Useful for component variants
|
|
24
|
+
*/
|
|
25
|
+
function createStyleMerger(baseStyle) {
|
|
26
|
+
return (...styles) => {
|
|
27
|
+
return mergeStyles(baseStyle, ...styles);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Conditionally applies styles based on boolean conditions
|
|
32
|
+
*/
|
|
33
|
+
function conditionalStyle(condition, trueStyle, falseStyle) {
|
|
34
|
+
return condition ? trueStyle : falseStyle;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates responsive values based on screen dimensions
|
|
38
|
+
*/
|
|
39
|
+
function responsiveValue(values, screenWidth) {
|
|
40
|
+
if (screenWidth >= 1280 && values.xl !== undefined)
|
|
41
|
+
return values.xl;
|
|
42
|
+
if (screenWidth >= 1024 && values.lg !== undefined)
|
|
43
|
+
return values.lg;
|
|
44
|
+
if (screenWidth >= 768 && values.md !== undefined)
|
|
45
|
+
return values.md;
|
|
46
|
+
if (screenWidth >= 640 && values.sm !== undefined)
|
|
47
|
+
return values.sm;
|
|
48
|
+
return values.default;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Creates platform-specific styles
|
|
52
|
+
*/
|
|
53
|
+
function platformStyle(styles) {
|
|
54
|
+
const Platform = require("react-native").Platform;
|
|
55
|
+
if (Platform.OS === "ios" && styles.ios)
|
|
56
|
+
return styles.ios;
|
|
57
|
+
if (Platform.OS === "android" && styles.android)
|
|
58
|
+
return styles.android;
|
|
59
|
+
if (Platform.OS === "web" && styles.web)
|
|
60
|
+
return styles.web;
|
|
61
|
+
return styles.default || {};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Converts hex color to rgba
|
|
65
|
+
*/
|
|
66
|
+
function hexToRgba(hex, alpha = 1) {
|
|
67
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
68
|
+
if (!result)
|
|
69
|
+
return hex;
|
|
70
|
+
const r = parseInt(result[1], 16);
|
|
71
|
+
const g = parseInt(result[2], 16);
|
|
72
|
+
const b = parseInt(result[3], 16);
|
|
73
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Creates a debounced function for performance
|
|
77
|
+
*/
|
|
78
|
+
function debounce(func, delay) {
|
|
79
|
+
let timeoutId;
|
|
80
|
+
return (...args) => {
|
|
81
|
+
clearTimeout(timeoutId);
|
|
82
|
+
timeoutId = setTimeout(() => func(...args), delay);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Creates a throttled function for performance
|
|
87
|
+
*/
|
|
88
|
+
function throttle(func, delay) {
|
|
89
|
+
let lastCall = 0;
|
|
90
|
+
return (...args) => {
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
if (now - lastCall >= delay) {
|
|
93
|
+
lastCall = now;
|
|
94
|
+
func(...args);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Type-safe component prop forwarding
|
|
100
|
+
*/
|
|
101
|
+
function forwardProps(props, omit) {
|
|
102
|
+
const result = { ...props };
|
|
103
|
+
omit.forEach((key) => delete result[key]);
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LivestreamProvider = LivestreamProvider;
|
|
4
|
+
exports.WebsocketWatcher = WebsocketWatcher;
|
|
5
|
+
exports.LivestreamPoller = LivestreamPoller;
|
|
6
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
const livestream_store_1 = require("../livestream-store");
|
|
9
|
+
const websocket_1 = require("./websocket");
|
|
10
|
+
function LivestreamProvider({ children, src, }) {
|
|
11
|
+
const context = (0, react_1.useContext)(livestream_store_1.LivestreamContext);
|
|
12
|
+
const store = (0, react_1.useRef)((0, livestream_store_1.makeLivestreamStore)()).current;
|
|
13
|
+
if (context) {
|
|
14
|
+
// this is ok, there's use cases for having one in another
|
|
15
|
+
// like having a player component that's independently usable
|
|
16
|
+
// but can also be embedded within an entire livestream page
|
|
17
|
+
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children });
|
|
18
|
+
}
|
|
19
|
+
window.livestreamStore = store;
|
|
20
|
+
return ((0, jsx_runtime_1.jsx)(livestream_store_1.LivestreamContext.Provider, { value: { store: store }, children: (0, jsx_runtime_1.jsx)(LivestreamPoller, { src: src, children: children }) }));
|
|
21
|
+
}
|
|
22
|
+
function WebsocketWatcher({ src }) {
|
|
23
|
+
(0, websocket_1.useLivestreamWebsocket)(src);
|
|
24
|
+
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {});
|
|
25
|
+
}
|
|
26
|
+
function LivestreamPoller({ children, src, }) {
|
|
27
|
+
// Websocket watcher is a sibling instead of a parent to avoid
|
|
28
|
+
// re-rendering when the websocket does stuff
|
|
29
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(WebsocketWatcher, { src: src }), children] }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useLivestreamWebsocket = useLivestreamWebsocket;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const react_use_websocket_1 = tslib_1.__importDefault(require("react-use-websocket"));
|
|
7
|
+
const livestream_store_1 = require("../livestream-store");
|
|
8
|
+
const streamplace_store_1 = require("../streamplace-store");
|
|
9
|
+
function useLivestreamWebsocket(src) {
|
|
10
|
+
const url = (0, streamplace_store_1.useUrl)();
|
|
11
|
+
const handleWebSocketMessages = (0, livestream_store_1.useHandleWebsocketMessages)();
|
|
12
|
+
let wsUrl = url.replace(/^http\:/, "ws:");
|
|
13
|
+
wsUrl = wsUrl.replace(/^https\:/, "wss:");
|
|
14
|
+
const ref = (0, react_1.useRef)([]);
|
|
15
|
+
const handle = (0, react_1.useRef)(null);
|
|
16
|
+
const { readyState } = (0, react_use_websocket_1.default)(`${wsUrl}/api/websocket/${src}`, {
|
|
17
|
+
reconnectInterval: 1000,
|
|
18
|
+
shouldReconnect: () => true,
|
|
19
|
+
onOpen: () => {
|
|
20
|
+
ref.current = [];
|
|
21
|
+
},
|
|
22
|
+
onError: (e) => {
|
|
23
|
+
console.log("onError", e);
|
|
24
|
+
},
|
|
25
|
+
// spamming the redux store with messages causes a zillion re-renders,
|
|
26
|
+
// so we batch them up a bit
|
|
27
|
+
onMessage: (msg) => {
|
|
28
|
+
try {
|
|
29
|
+
const data = JSON.parse(msg.data);
|
|
30
|
+
ref.current.push(data);
|
|
31
|
+
if (handle.current) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
handle.current = setTimeout(() => {
|
|
35
|
+
handleWebSocketMessages(ref.current);
|
|
36
|
+
ref.current = [];
|
|
37
|
+
handle.current = null;
|
|
38
|
+
}, 250);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
console.log("onMessage parse error", e);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reduceChat = exports.useReportChatMessage = exports.useSubmitReport = exports.reduceChatIncremental = exports.useDeleteChatMessage = exports.useCreateChatMessage = exports.useAddPendingHide = exports.usePendingHides = exports.useSetReplyToMessage = exports.useReplyToMessage = void 0;
|
|
4
|
+
const api_1 = require("@atproto/api");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const streamplace_store_1 = require("../streamplace-store");
|
|
7
|
+
const xrpc_1 = require("../streamplace-store/xrpc");
|
|
8
|
+
const livestream_store_1 = require("./livestream-store");
|
|
9
|
+
const useReplyToMessage = () => (0, livestream_store_1.useLivestreamStore)((state) => state.replyToMessage);
|
|
10
|
+
exports.useReplyToMessage = useReplyToMessage;
|
|
11
|
+
const useSetReplyToMessage = () => {
|
|
12
|
+
const store = (0, livestream_store_1.getStoreFromContext)();
|
|
13
|
+
return (0, react_1.useCallback)((message) => {
|
|
14
|
+
store.setState({ replyToMessage: message });
|
|
15
|
+
}, [store]);
|
|
16
|
+
};
|
|
17
|
+
exports.useSetReplyToMessage = useSetReplyToMessage;
|
|
18
|
+
const usePendingHides = () => (0, livestream_store_1.useLivestreamStore)((state) => state.pendingHides);
|
|
19
|
+
exports.usePendingHides = usePendingHides;
|
|
20
|
+
const useAddPendingHide = () => {
|
|
21
|
+
const store = (0, livestream_store_1.getStoreFromContext)();
|
|
22
|
+
return (0, react_1.useCallback)((messageUri) => {
|
|
23
|
+
const state = store.getState();
|
|
24
|
+
if (!state.pendingHides.includes(messageUri)) {
|
|
25
|
+
const newPendingHides = [...state.pendingHides, messageUri];
|
|
26
|
+
const newState = (0, exports.reduceChat)(state, [], [], [messageUri]);
|
|
27
|
+
store.setState({
|
|
28
|
+
...newState,
|
|
29
|
+
pendingHides: newPendingHides,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}, [store]);
|
|
33
|
+
};
|
|
34
|
+
exports.useAddPendingHide = useAddPendingHide;
|
|
35
|
+
const useCreateChatMessage = () => {
|
|
36
|
+
const pdsAgent = (0, xrpc_1.usePDSAgent)();
|
|
37
|
+
const store = (0, livestream_store_1.getStoreFromContext)();
|
|
38
|
+
const userDID = (0, streamplace_store_1.useDID)();
|
|
39
|
+
const userHandle = (0, streamplace_store_1.useHandle)();
|
|
40
|
+
const chatProfile = (0, streamplace_store_1.useChatProfile)();
|
|
41
|
+
return async (msg) => {
|
|
42
|
+
if (!pdsAgent || !userDID) {
|
|
43
|
+
throw new Error("No PDS agent or user DID found");
|
|
44
|
+
}
|
|
45
|
+
let state = store.getState();
|
|
46
|
+
const streamerProfile = state.profile;
|
|
47
|
+
if (!streamerProfile) {
|
|
48
|
+
throw new Error("Profile not found");
|
|
49
|
+
}
|
|
50
|
+
const rt = new api_1.RichText({ text: msg.text });
|
|
51
|
+
await rt.detectFacets(pdsAgent);
|
|
52
|
+
const record = {
|
|
53
|
+
text: msg.text,
|
|
54
|
+
createdAt: new Date().toISOString(),
|
|
55
|
+
streamer: streamerProfile.did,
|
|
56
|
+
facets: rt.facets,
|
|
57
|
+
...(msg.reply
|
|
58
|
+
? {
|
|
59
|
+
reply: {
|
|
60
|
+
root: {
|
|
61
|
+
cid: msg.reply.cid,
|
|
62
|
+
uri: msg.reply.uri,
|
|
63
|
+
},
|
|
64
|
+
parent: {
|
|
65
|
+
cid: msg.reply.cid,
|
|
66
|
+
uri: msg.reply.uri,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
: {}),
|
|
71
|
+
};
|
|
72
|
+
const localChat = {
|
|
73
|
+
uri: `local-${Date.now()}`,
|
|
74
|
+
cid: "",
|
|
75
|
+
author: {
|
|
76
|
+
did: userDID,
|
|
77
|
+
handle: userHandle || userDID,
|
|
78
|
+
},
|
|
79
|
+
record: record,
|
|
80
|
+
indexedAt: new Date().toISOString(),
|
|
81
|
+
chatProfile: chatProfile || undefined,
|
|
82
|
+
};
|
|
83
|
+
state = (0, exports.reduceChat)(state, [localChat], [], []);
|
|
84
|
+
store.setState(state);
|
|
85
|
+
await pdsAgent.com.atproto.repo.createRecord({
|
|
86
|
+
repo: userDID,
|
|
87
|
+
collection: "place.stream.chat.message",
|
|
88
|
+
record,
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
exports.useCreateChatMessage = useCreateChatMessage;
|
|
93
|
+
const useDeleteChatMessage = () => {
|
|
94
|
+
const pdsAgent = (0, xrpc_1.usePDSAgent)();
|
|
95
|
+
if (!pdsAgent) {
|
|
96
|
+
throw new Error("No PDS agent found");
|
|
97
|
+
}
|
|
98
|
+
const userDID = (0, streamplace_store_1.useDID)();
|
|
99
|
+
if (!userDID) {
|
|
100
|
+
throw new Error("No user DID found");
|
|
101
|
+
}
|
|
102
|
+
return async (uri) => {
|
|
103
|
+
const rkey = uri.split("/").pop();
|
|
104
|
+
if (!rkey) {
|
|
105
|
+
throw new Error("No rkey found");
|
|
106
|
+
}
|
|
107
|
+
return await pdsAgent.com.atproto.repo.deleteRecord({
|
|
108
|
+
repo: userDID,
|
|
109
|
+
collection: "place.stream.chat.message",
|
|
110
|
+
rkey: rkey,
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
exports.useDeleteChatMessage = useDeleteChatMessage;
|
|
115
|
+
const buildSortedChatList = (chatIndex, existingChatList, newMessages, removedKeys) => {
|
|
116
|
+
const sortedKeys = Object.keys(chatIndex).sort((a, b) => {
|
|
117
|
+
const aTime = parseInt(a.split("-")[0], 10);
|
|
118
|
+
const bTime = parseInt(b.split("-")[0], 10);
|
|
119
|
+
return bTime - aTime;
|
|
120
|
+
});
|
|
121
|
+
return sortedKeys
|
|
122
|
+
.map((key) => chatIndex[key])
|
|
123
|
+
.filter((msg) => !removedKeys.has(msg.uri));
|
|
124
|
+
};
|
|
125
|
+
const profileIsDifferent = (newProfile, oldProfile) => {
|
|
126
|
+
if (!oldProfile) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (!newProfile) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
if (!oldProfile.color) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (!newProfile.color) {
|
|
136
|
+
// idk. shouldn't happen.
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const { red: newRed, green: newGreen, blue: newBlue } = newProfile.color;
|
|
140
|
+
const { red: oldRed, green: oldGreen, blue: oldBlue } = oldProfile.color;
|
|
141
|
+
return newRed !== oldRed || newGreen !== oldGreen || newBlue !== oldBlue;
|
|
142
|
+
};
|
|
143
|
+
const reduceChatIncremental = (state, newMessages, blocks, hideUris = []) => {
|
|
144
|
+
if (newMessages.length === 0 &&
|
|
145
|
+
blocks.length === 0 &&
|
|
146
|
+
hideUris.length === 0) {
|
|
147
|
+
return state;
|
|
148
|
+
}
|
|
149
|
+
const newChatIndex = { ...state.chatIndex };
|
|
150
|
+
const newAuthors = { ...state.authors };
|
|
151
|
+
let hasChanges = false;
|
|
152
|
+
const removedKeys = new Set();
|
|
153
|
+
console.log("newMessages", newMessages);
|
|
154
|
+
for (const msg of newMessages) {
|
|
155
|
+
if (msg.deleted) {
|
|
156
|
+
hasChanges = true;
|
|
157
|
+
console.log("deleted", msg.uri);
|
|
158
|
+
removedKeys.add(msg.uri);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
newMessages = newMessages.filter((msg) => msg.deleted !== true);
|
|
162
|
+
// handle blocks
|
|
163
|
+
if (blocks.length > 0) {
|
|
164
|
+
const blockedDIDs = new Set(blocks.map((block) => block.record.subject));
|
|
165
|
+
for (const [key, message] of Object.entries(newChatIndex)) {
|
|
166
|
+
if (blockedDIDs.has(message.author.did)) {
|
|
167
|
+
delete newChatIndex[key];
|
|
168
|
+
removedKeys.add(key);
|
|
169
|
+
hasChanges = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (hideUris.length > 0) {
|
|
174
|
+
for (const [key, message] of Object.entries(newChatIndex)) {
|
|
175
|
+
if (hideUris.includes(message.uri)) {
|
|
176
|
+
delete newChatIndex[key];
|
|
177
|
+
removedKeys.add(key);
|
|
178
|
+
hasChanges = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const messagesToAdd = [];
|
|
183
|
+
for (const message of newMessages) {
|
|
184
|
+
// don't worry about messages that will be hidden
|
|
185
|
+
if (state.pendingHides.includes(message.uri)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const date = new Date(message.record.createdAt);
|
|
189
|
+
const key = `${date.getTime()}-${message.uri}`;
|
|
190
|
+
// only change the ref if the profile is different to avoid re-renders elsewhere
|
|
191
|
+
if (profileIsDifferent(message.chatProfile, newAuthors[message.author.did])) {
|
|
192
|
+
newAuthors[message.author.did] = message.chatProfile;
|
|
193
|
+
}
|
|
194
|
+
// skip messages we already have
|
|
195
|
+
if (newChatIndex[key] && newChatIndex[key].uri === message.uri) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// if we have a local message, replace it with the new one
|
|
199
|
+
if (!message.uri.startsWith("local-")) {
|
|
200
|
+
const existingLocalKey = Object.keys(newChatIndex).find((k) => {
|
|
201
|
+
const msg = newChatIndex[k];
|
|
202
|
+
return (msg.uri.startsWith("local-") &&
|
|
203
|
+
msg.record.text === message.record.text &&
|
|
204
|
+
msg.author.did === message.author.did &&
|
|
205
|
+
Math.abs(new Date(msg.record.createdAt).getTime() - date.getTime()) <
|
|
206
|
+
10000 // Within 10 seconds
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
if (existingLocalKey) {
|
|
210
|
+
delete newChatIndex[existingLocalKey];
|
|
211
|
+
removedKeys.add(existingLocalKey);
|
|
212
|
+
hasChanges = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// add reply info
|
|
216
|
+
let processedMessage = message;
|
|
217
|
+
if (message.record.reply) {
|
|
218
|
+
const reply = message.record.reply;
|
|
219
|
+
const parentUri = reply?.parent?.uri || reply?.root?.uri;
|
|
220
|
+
if (parentUri) {
|
|
221
|
+
const parentMsgKey = Object.keys(newChatIndex).find((k) => newChatIndex[k].uri === parentUri);
|
|
222
|
+
if (parentMsgKey) {
|
|
223
|
+
const parentMsg = newChatIndex[parentMsgKey];
|
|
224
|
+
// Don't allow replies to system messages
|
|
225
|
+
if (parentMsg.author.did !== "did:sys:system") {
|
|
226
|
+
processedMessage = {
|
|
227
|
+
...message,
|
|
228
|
+
replyTo: {
|
|
229
|
+
cid: parentMsg.cid,
|
|
230
|
+
uri: parentMsg.uri,
|
|
231
|
+
author: parentMsg.author,
|
|
232
|
+
record: parentMsg.record,
|
|
233
|
+
chatProfile: parentMsg.chatProfile,
|
|
234
|
+
indexedAt: parentMsg.indexedAt,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
messagesToAdd.push({ key, message: processedMessage });
|
|
242
|
+
hasChanges = true;
|
|
243
|
+
}
|
|
244
|
+
// Add new messages to index
|
|
245
|
+
for (const { key, message } of messagesToAdd) {
|
|
246
|
+
newChatIndex[key] = message;
|
|
247
|
+
}
|
|
248
|
+
// only rebuild if we have changes
|
|
249
|
+
if (!hasChanges) {
|
|
250
|
+
return state;
|
|
251
|
+
}
|
|
252
|
+
// Build the new sorted chat list efficiently
|
|
253
|
+
const newChatList = buildSortedChatList(newChatIndex, state.chat, messagesToAdd, removedKeys);
|
|
254
|
+
// Clean up pendingHides - remove URIs that we've now processed
|
|
255
|
+
let newPendingHides = state.pendingHides;
|
|
256
|
+
if (hideUris.length > 0) {
|
|
257
|
+
newPendingHides = state.pendingHides.filter((uri) => !hideUris.includes(uri));
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
...state,
|
|
261
|
+
authors: newAuthors,
|
|
262
|
+
chatIndex: newChatIndex,
|
|
263
|
+
chat: newChatList,
|
|
264
|
+
pendingHides: newPendingHides,
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
exports.reduceChatIncremental = reduceChatIncremental;
|
|
268
|
+
const useSubmitReport = () => {
|
|
269
|
+
const pdsAgent = (0, xrpc_1.usePDSAgent)();
|
|
270
|
+
const userDID = (0, streamplace_store_1.useDID)();
|
|
271
|
+
return (0, react_1.useCallback)(async (subject, reasonType, reason,
|
|
272
|
+
// no clue about this
|
|
273
|
+
moderationSvcDid = "did:web:stream.place") => {
|
|
274
|
+
if (!pdsAgent || !userDID) {
|
|
275
|
+
throw new Error("No PDS agent or user DID found");
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const response = await pdsAgent.com.atproto.moderation.createReport({
|
|
279
|
+
reasonType,
|
|
280
|
+
reason,
|
|
281
|
+
subject: subject,
|
|
282
|
+
}, {
|
|
283
|
+
headers: {
|
|
284
|
+
// "atproto-proxy": `${userDID}#atproto_labeler`,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
return response;
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
console.error("Failed to submit report:", error);
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}, [pdsAgent, userDID]);
|
|
294
|
+
};
|
|
295
|
+
exports.useSubmitReport = useSubmitReport;
|
|
296
|
+
const useReportChatMessage = () => {
|
|
297
|
+
const submitReport = (0, exports.useSubmitReport)();
|
|
298
|
+
return (0, react_1.useCallback)(async (message, reasonType, reason) => {
|
|
299
|
+
const reportSubject = {
|
|
300
|
+
$type: "com.atproto.repo.strongRef",
|
|
301
|
+
uri: message.uri,
|
|
302
|
+
cid: message.cid,
|
|
303
|
+
};
|
|
304
|
+
return await submitReport(reportSubject, reasonType, reason);
|
|
305
|
+
}, [submitReport]);
|
|
306
|
+
};
|
|
307
|
+
exports.useReportChatMessage = useReportChatMessage;
|
|
308
|
+
exports.reduceChat = exports.reduceChatIncremental;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
tslib_1.__exportStar(require("./chat"), exports);
|
|
5
|
+
tslib_1.__exportStar(require("./context"), exports);
|
|
6
|
+
tslib_1.__exportStar(require("./livestream-store"), exports);
|
|
7
|
+
tslib_1.__exportStar(require("./stream-key"), exports);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useRenditions = exports.useSegment = exports.useLivestream = exports.useViewers = exports.useProfile = exports.useChat = exports.useHandleWebsocketMessages = exports.makeLivestreamStore = void 0;
|
|
4
|
+
exports.getStoreFromContext = getStoreFromContext;
|
|
5
|
+
exports.useLivestreamStore = useLivestreamStore;
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const zustand_1 = require("zustand");
|
|
8
|
+
const context_1 = require("./context");
|
|
9
|
+
const websocket_consumer_1 = require("./websocket-consumer");
|
|
10
|
+
const makeLivestreamStore = () => {
|
|
11
|
+
return (0, zustand_1.createStore)()((set) => ({
|
|
12
|
+
profile: null,
|
|
13
|
+
chatIndex: {},
|
|
14
|
+
chat: [],
|
|
15
|
+
livestream: null,
|
|
16
|
+
viewers: null,
|
|
17
|
+
pendingHides: [],
|
|
18
|
+
segment: null,
|
|
19
|
+
renditions: [],
|
|
20
|
+
replyToMessage: null,
|
|
21
|
+
streamKey: null,
|
|
22
|
+
setStreamKey: (sk) => set({ streamKey: sk }),
|
|
23
|
+
authors: {},
|
|
24
|
+
recentSegments: [],
|
|
25
|
+
problems: [],
|
|
26
|
+
}));
|
|
27
|
+
};
|
|
28
|
+
exports.makeLivestreamStore = makeLivestreamStore;
|
|
29
|
+
function getStoreFromContext() {
|
|
30
|
+
const context = (0, react_1.useContext)(context_1.LivestreamContext);
|
|
31
|
+
if (!context) {
|
|
32
|
+
throw new Error("useLivestreamStore must be used within a LivestreamProvider");
|
|
33
|
+
}
|
|
34
|
+
return context.store;
|
|
35
|
+
}
|
|
36
|
+
function useLivestreamStore(selector) {
|
|
37
|
+
const store = getStoreFromContext();
|
|
38
|
+
return (0, zustand_1.useStore)(store, selector);
|
|
39
|
+
}
|
|
40
|
+
const useHandleWebsocketMessages = () => {
|
|
41
|
+
const store = getStoreFromContext();
|
|
42
|
+
return (messages) => {
|
|
43
|
+
store.setState((state) => (0, websocket_consumer_1.handleWebSocketMessages)(state, messages));
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
exports.useHandleWebsocketMessages = useHandleWebsocketMessages;
|
|
47
|
+
const useChat = () => useLivestreamStore((x) => x.chat);
|
|
48
|
+
exports.useChat = useChat;
|
|
49
|
+
const useProfile = () => useLivestreamStore((x) => x.profile);
|
|
50
|
+
exports.useProfile = useProfile;
|
|
51
|
+
const useViewers = () => useLivestreamStore((x) => x.viewers);
|
|
52
|
+
exports.useViewers = useViewers;
|
|
53
|
+
const useLivestream = () => useLivestreamStore((x) => x.livestream);
|
|
54
|
+
exports.useLivestream = useLivestream;
|
|
55
|
+
const useSegment = () => useLivestreamStore((x) => x.segment);
|
|
56
|
+
exports.useSegment = useSegment;
|
|
57
|
+
const useRenditions = () => useLivestreamStore((x) => x.renditions);
|
|
58
|
+
exports.useRenditions = useRenditions;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findProblems = void 0;
|
|
4
|
+
const VARIANCE_THRESHOLD = 0.5;
|
|
5
|
+
const DURATION_THRESHOLD = 5000000000; // 5s in ns
|
|
6
|
+
const detectVariableSegmentLength = (segments) => {
|
|
7
|
+
if (segments.length < 3) {
|
|
8
|
+
// Need at least 3 segments to detect variability
|
|
9
|
+
return { variable: false, duration: false };
|
|
10
|
+
}
|
|
11
|
+
const durations = segments
|
|
12
|
+
.map((segment) => segment.duration)
|
|
13
|
+
.filter((duration) => duration !== undefined && duration > 0);
|
|
14
|
+
if (durations.length < 3) {
|
|
15
|
+
return { variable: false, duration: false };
|
|
16
|
+
}
|
|
17
|
+
// Calculate mean
|
|
18
|
+
const mean = durations.reduce((sum, duration) => sum + duration, 0) /
|
|
19
|
+
durations.length;
|
|
20
|
+
// Calculate standard deviation
|
|
21
|
+
const variance = durations.reduce((sum, duration) => {
|
|
22
|
+
const diff = duration - mean;
|
|
23
|
+
return sum + diff * diff;
|
|
24
|
+
}, 0) / durations.length;
|
|
25
|
+
const stdDev = Math.sqrt(variance);
|
|
26
|
+
// Calculate coefficient of variation (CV)
|
|
27
|
+
const cv = stdDev / mean;
|
|
28
|
+
// CV > 0.5 indicates high variability
|
|
29
|
+
// This threshold can be adjusted based on testing
|
|
30
|
+
return {
|
|
31
|
+
variable: cv > VARIANCE_THRESHOLD,
|
|
32
|
+
duration: mean > DURATION_THRESHOLD,
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const findProblems = (segments) => {
|
|
36
|
+
const problems = [];
|
|
37
|
+
let hasBFrames = false;
|
|
38
|
+
for (const segment of segments) {
|
|
39
|
+
const video = segment.video?.[0];
|
|
40
|
+
if (!video) {
|
|
41
|
+
// i mean yes this is a problem but it can't happen yet
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (video.bframes === true) {
|
|
45
|
+
hasBFrames = true;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (hasBFrames) {
|
|
50
|
+
problems.push({
|
|
51
|
+
code: "bframes",
|
|
52
|
+
message: "Your stream contains B-Frames, which are not supported in Streamplace. Your stream will stutter.",
|
|
53
|
+
severity: "error",
|
|
54
|
+
link: "https://stream.place/docs/guides/start-streaming/obs/#obs-configuration",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const { variable, duration } = detectVariableSegmentLength(segments);
|
|
58
|
+
if (variable) {
|
|
59
|
+
problems.push({
|
|
60
|
+
code: "variable_segment_length",
|
|
61
|
+
message: "Your stream contains variable segment lengths, which may cause playback issues.",
|
|
62
|
+
severity: "warning",
|
|
63
|
+
link: "https://stream.place/docs/guides/start-streaming/obs/#obs-configuration",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (duration) {
|
|
67
|
+
problems.push({
|
|
68
|
+
code: "long_segments",
|
|
69
|
+
message: "Your stream contains long segments (>5s). This will work fine, but increases the delay of the livestream.",
|
|
70
|
+
severity: "warning",
|
|
71
|
+
link: "https://stream.place/docs/guides/start-streaming/obs/#obs-configuration",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return problems;
|
|
75
|
+
};
|
|
76
|
+
exports.findProblems = findProblems;
|