@streamplace/components 0.0.1 → 0.6.37
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/LICENSE +18 -0
- package/README.md +35 -0
- package/dist/index.js +6 -0
- package/dist/livestream-provider/index.js +20 -0
- package/dist/livestream-provider/websocket.js +41 -0
- package/dist/livestream-store/chat.js +162 -0
- package/dist/livestream-store/context.js +2 -0
- package/dist/livestream-store/index.js +3 -0
- package/dist/livestream-store/livestream-state.js +1 -0
- package/dist/livestream-store/livestream-store.js +39 -0
- package/dist/livestream-store/websocket-consumer.js +55 -0
- package/dist/player-store/context.js +2 -0
- package/dist/player-store/index.js +6 -0
- package/dist/player-store/player-provider.js +53 -0
- package/dist/player-store/player-state.js +22 -0
- package/dist/player-store/player-store.js +146 -0
- package/dist/player-store/single-player-provider.js +109 -0
- package/dist/streamplace-provider/context.js +2 -0
- package/dist/streamplace-provider/index.js +16 -0
- package/dist/streamplace-provider/poller.js +46 -0
- package/dist/streamplace-provider/xrpc.js +0 -0
- package/dist/streamplace-store/index.js +2 -0
- package/dist/streamplace-store/streamplace-store.js +37 -0
- package/dist/streamplace-store/user.js +47 -0
- package/dist/streamplace-store/xrpc.js +12 -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 +34 -8
- package/src/index.tsx +6 -0
- package/src/livestream-provider/index.tsx +37 -0
- package/src/livestream-provider/websocket.tsx +47 -0
- package/src/livestream-store/chat.tsx +224 -0
- package/src/livestream-store/context.tsx +10 -0
- package/src/livestream-store/index.tsx +3 -0
- package/src/livestream-store/livestream-state.tsx +18 -0
- package/src/livestream-store/livestream-store.tsx +56 -0
- package/src/livestream-store/websocket-consumer.tsx +62 -0
- package/src/player-store/context.tsx +11 -0
- package/src/player-store/index.tsx +6 -0
- package/src/player-store/player-provider.tsx +90 -0
- package/src/player-store/player-state.tsx +159 -0
- package/src/player-store/player-store.tsx +217 -0
- package/src/player-store/single-player-provider.tsx +181 -0
- package/src/streamplace-provider/context.tsx +10 -0
- package/src/streamplace-provider/index.tsx +32 -0
- package/src/streamplace-provider/poller.tsx +55 -0
- package/src/streamplace-provider/xrpc.tsx +0 -0
- package/src/streamplace-store/index.tsx +2 -0
- package/src/streamplace-store/streamplace-store.tsx +89 -0
- package/src/streamplace-store/user.tsx +57 -0
- package/src/streamplace-store/xrpc.tsx +15 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { createContext, useContext, useMemo } from "react";
|
|
3
|
+
import { useStore } from "zustand";
|
|
4
|
+
import { usePlayerContext } from "../player-store";
|
|
5
|
+
import { PlayerProtocol } from "./player-state";
|
|
6
|
+
const SinglePlayerContext = createContext(null);
|
|
7
|
+
/**
|
|
8
|
+
* Provider component for a single player that creates a scoped context
|
|
9
|
+
* This allows components to access a specific player's state without passing IDs around
|
|
10
|
+
*/
|
|
11
|
+
export const SinglePlayerProvider = ({ children, playerId: providedPlayerId, protocol = PlayerProtocol.WEBRTC, rendition = "auto", }) => {
|
|
12
|
+
const { players, createPlayer } = usePlayerContext();
|
|
13
|
+
// Create or get a player ID
|
|
14
|
+
const playerId = useMemo(() => {
|
|
15
|
+
// If a player ID is provided and exists, use it
|
|
16
|
+
if (providedPlayerId && players[providedPlayerId]) {
|
|
17
|
+
return providedPlayerId;
|
|
18
|
+
}
|
|
19
|
+
// If a player ID is provided but doesn't exist, create it
|
|
20
|
+
if (providedPlayerId) {
|
|
21
|
+
return createPlayer(providedPlayerId);
|
|
22
|
+
}
|
|
23
|
+
// Otherwise create a new player
|
|
24
|
+
return createPlayer();
|
|
25
|
+
}, [providedPlayerId, players, createPlayer]);
|
|
26
|
+
// Get the player store
|
|
27
|
+
const playerStore = useMemo(() => {
|
|
28
|
+
return players[playerId];
|
|
29
|
+
}, [players, playerId]);
|
|
30
|
+
// Set initial protocol and rendition if provided
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
if (protocol) {
|
|
33
|
+
playerStore.setState((state) => ({
|
|
34
|
+
...state,
|
|
35
|
+
protocol,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
if (rendition) {
|
|
39
|
+
playerStore.setState((state) => ({
|
|
40
|
+
...state,
|
|
41
|
+
selectedRendition: rendition,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
}, [playerStore, protocol, rendition]);
|
|
45
|
+
// Create context value
|
|
46
|
+
const contextValue = useMemo(() => ({
|
|
47
|
+
playerId,
|
|
48
|
+
playerStore,
|
|
49
|
+
}), [playerId, playerStore]);
|
|
50
|
+
return (_jsx(SinglePlayerContext.Provider, { value: contextValue, children: children }));
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Hook to access the current single player context
|
|
54
|
+
*/
|
|
55
|
+
export function useSinglePlayerContext() {
|
|
56
|
+
const context = useContext(SinglePlayerContext);
|
|
57
|
+
if (!context) {
|
|
58
|
+
throw new Error("useSinglePlayerContext must be used within a SinglePlayerProvider");
|
|
59
|
+
}
|
|
60
|
+
return context;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Hook to access the current player ID from the single player context
|
|
64
|
+
*/
|
|
65
|
+
export function useCurrentPlayerId() {
|
|
66
|
+
const { playerId } = useSinglePlayerContext();
|
|
67
|
+
return playerId;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Hook to access state from the current player without needing to specify the ID
|
|
71
|
+
*/
|
|
72
|
+
export function useCurrentPlayerStore(selector) {
|
|
73
|
+
const { playerStore } = useSinglePlayerContext();
|
|
74
|
+
return useStore(playerStore, selector);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Hook to get the protocol of the current player
|
|
78
|
+
*/
|
|
79
|
+
export function useCurrentPlayerProtocol() {
|
|
80
|
+
return useCurrentPlayerStore((state) => [state.protocol, state.setProtocol]);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Hook to get the selected rendition of the current player
|
|
84
|
+
*/
|
|
85
|
+
export function useCurrentPlayerRendition() {
|
|
86
|
+
return useCurrentPlayerStore((state) => [state.selectedRendition, state.setSelectedRendition]);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Hook to get the ingest state of the current player
|
|
90
|
+
*/
|
|
91
|
+
export function useCurrentPlayerIngest() {
|
|
92
|
+
return useCurrentPlayerStore((state) => ({
|
|
93
|
+
starting: state.ingestStarting,
|
|
94
|
+
setStarting: state.setIngestStarting,
|
|
95
|
+
connectionState: state.ingestConnectionState,
|
|
96
|
+
setConnectionState: state.setIngestConnectionState,
|
|
97
|
+
startedTimestamp: state.ingestStarted,
|
|
98
|
+
setStartedTimestamp: state.setIngestStarted,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* HOC to wrap components with a SinglePlayerProvider
|
|
103
|
+
*/
|
|
104
|
+
export function withSinglePlayer(Component) {
|
|
105
|
+
return function WithSinglePlayer(props) {
|
|
106
|
+
const { playerId, protocol, rendition, ...componentProps } = props;
|
|
107
|
+
return (_jsx(SinglePlayerProvider, { playerId: playerId, protocol: protocol, rendition: rendition, children: _jsx(Component, { ...componentProps }) }));
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { makeStreamplaceStore } from "../streamplace-store/streamplace-store";
|
|
4
|
+
import { StreamplaceContext } from "./context";
|
|
5
|
+
import Poller from "./poller";
|
|
6
|
+
export function StreamplaceProvider({ children, url, oauthSession, }) {
|
|
7
|
+
// todo: handle url changes?
|
|
8
|
+
const store = useRef(makeStreamplaceStore({ url })).current;
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
store.setState({ url });
|
|
11
|
+
}, [url]);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
store.setState({ oauthSession });
|
|
14
|
+
}, [oauthSession]);
|
|
15
|
+
return (_jsx(StreamplaceContext.Provider, { value: { store: store }, children: _jsx(Poller, { children: children }) }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { StreamplaceAgent } from "streamplace";
|
|
4
|
+
import { useDID, useGetBskyProfile, useGetChatProfile, useStreamplaceStore, } from "../streamplace-store";
|
|
5
|
+
import { usePDSAgent } from "../streamplace-store/xrpc";
|
|
6
|
+
export default function Poller({ children }) {
|
|
7
|
+
const url = useStreamplaceStore((state) => state.url);
|
|
8
|
+
const setLiveUsers = useStreamplaceStore((state) => state.setLiveUsers);
|
|
9
|
+
const did = useDID();
|
|
10
|
+
const pdsAgent = usePDSAgent();
|
|
11
|
+
const getChatProfile = useGetChatProfile();
|
|
12
|
+
const getBskyProfile = useGetBskyProfile();
|
|
13
|
+
const liveUserRefresh = useStreamplaceStore((state) => state.liveUsersRefresh);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (pdsAgent && did) {
|
|
16
|
+
getChatProfile();
|
|
17
|
+
getBskyProfile();
|
|
18
|
+
}
|
|
19
|
+
}, [pdsAgent, did]);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const agent = new StreamplaceAgent(url);
|
|
22
|
+
const go = async () => {
|
|
23
|
+
setLiveUsers({
|
|
24
|
+
liveUsersLoading: true,
|
|
25
|
+
});
|
|
26
|
+
try {
|
|
27
|
+
const res = await agent.place.stream.live.getLiveUsers();
|
|
28
|
+
setLiveUsers({
|
|
29
|
+
liveUsers: res.data.streams || [],
|
|
30
|
+
liveUsersLoading: false,
|
|
31
|
+
liveUsersError: null,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
setLiveUsers({
|
|
36
|
+
liveUsersLoading: false,
|
|
37
|
+
liveUsersError: e.message,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
go();
|
|
42
|
+
const handle = setInterval(go, 3000);
|
|
43
|
+
return () => clearInterval(handle);
|
|
44
|
+
}, [url, liveUserRefresh]);
|
|
45
|
+
return _jsx(_Fragment, { children: children });
|
|
46
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { createStore, useStore } from "zustand";
|
|
3
|
+
import { StreamplaceContext } from "../streamplace-provider/context";
|
|
4
|
+
export const makeStreamplaceStore = ({ url, }) => {
|
|
5
|
+
return createStore()((set) => ({
|
|
6
|
+
url,
|
|
7
|
+
liveUsers: null,
|
|
8
|
+
setLiveUsers: (opts) => {
|
|
9
|
+
set({
|
|
10
|
+
...opts,
|
|
11
|
+
});
|
|
12
|
+
},
|
|
13
|
+
liveUsersRefresh: 0,
|
|
14
|
+
liveUsersLoading: true,
|
|
15
|
+
liveUsersError: null,
|
|
16
|
+
oauthSession: null,
|
|
17
|
+
handle: null,
|
|
18
|
+
chatProfile: null,
|
|
19
|
+
}));
|
|
20
|
+
};
|
|
21
|
+
export function getStreamplaceStoreFromContext() {
|
|
22
|
+
const context = useContext(StreamplaceContext);
|
|
23
|
+
if (!context) {
|
|
24
|
+
throw new Error("useStreamplaceStore must be used within a StreamplaceProvider");
|
|
25
|
+
}
|
|
26
|
+
return context.store;
|
|
27
|
+
}
|
|
28
|
+
export function useStreamplaceStore(selector) {
|
|
29
|
+
return useStore(getStreamplaceStoreFromContext(), selector);
|
|
30
|
+
}
|
|
31
|
+
export const useUrl = () => useStreamplaceStore((x) => x.url);
|
|
32
|
+
export const useDID = () => useStreamplaceStore((x) => x.oauthSession?.did);
|
|
33
|
+
export const useHandle = () => useStreamplaceStore((x) => x.handle);
|
|
34
|
+
export const useSetHandle = () => {
|
|
35
|
+
const store = getStreamplaceStoreFromContext();
|
|
36
|
+
return (handle) => store.setState({ handle });
|
|
37
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { PlaceStreamChatProfile } from "streamplace";
|
|
2
|
+
import { getStreamplaceStoreFromContext, useDID, useStreamplaceStore, } from "./streamplace-store";
|
|
3
|
+
import { usePDSAgent } from "./xrpc";
|
|
4
|
+
export function useGetChatProfile() {
|
|
5
|
+
const did = useDID();
|
|
6
|
+
const pdsAgent = usePDSAgent();
|
|
7
|
+
const store = getStreamplaceStoreFromContext();
|
|
8
|
+
return async () => {
|
|
9
|
+
if (!did || !pdsAgent) {
|
|
10
|
+
throw new Error("No DID or PDS agent");
|
|
11
|
+
}
|
|
12
|
+
const res = await pdsAgent.com.atproto.repo.getRecord({
|
|
13
|
+
repo: did,
|
|
14
|
+
collection: "place.stream.chat.profile",
|
|
15
|
+
rkey: "self",
|
|
16
|
+
});
|
|
17
|
+
if (!res.success) {
|
|
18
|
+
throw new Error("Failed to get chat profile record");
|
|
19
|
+
}
|
|
20
|
+
if (PlaceStreamChatProfile.isRecord(res.data.value)) {
|
|
21
|
+
store.setState({ chatProfile: res.data.value });
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.log("not a record", res.data.value);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function useGetBskyProfile() {
|
|
29
|
+
const did = useDID();
|
|
30
|
+
const pdsAgent = usePDSAgent();
|
|
31
|
+
const store = getStreamplaceStoreFromContext();
|
|
32
|
+
return async () => {
|
|
33
|
+
if (!did || !pdsAgent) {
|
|
34
|
+
throw new Error("No DID or PDS agent");
|
|
35
|
+
}
|
|
36
|
+
const res = await pdsAgent.app.bsky.actor.getProfile({
|
|
37
|
+
actor: did,
|
|
38
|
+
});
|
|
39
|
+
if (!res.success) {
|
|
40
|
+
throw new Error("Failed to get chat profile record");
|
|
41
|
+
}
|
|
42
|
+
store.setState({ handle: res.data.handle });
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function useChatProfile() {
|
|
46
|
+
return useStreamplaceStore((x) => x.chatProfile);
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { StreamplaceAgent } from "streamplace";
|
|
3
|
+
import { useStreamplaceStore } from ".";
|
|
4
|
+
export function usePDSAgent() {
|
|
5
|
+
const oauthSession = useStreamplaceStore((state) => state.oauthSession);
|
|
6
|
+
return useMemo(() => {
|
|
7
|
+
if (!oauthSession) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return new StreamplaceAgent(oauthSession);
|
|
11
|
+
}, [oauthSession]);
|
|
12
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamplace/components",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "",
|
|
5
|
-
"
|
|
3
|
+
"version": "0.6.37",
|
|
4
|
+
"description": "Streamplace React (Native) Components",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "src/index.tsx",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.mjs",
|
|
11
|
+
"default": "./dist/index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
6
14
|
"scripts": {
|
|
7
|
-
"
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"postinstall": "pnpm run build",
|
|
17
|
+
"start": "tsc --watch --preserveWatchOutput"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"streamplace"
|
|
21
|
+
],
|
|
22
|
+
"author": "Streamplace",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"packageManager": "pnpm@10.11.0",
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"tsup": "^8.5.0"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@atproto/api": "^0.15.7",
|
|
30
|
+
"react-native": "0.76.2",
|
|
31
|
+
"react-use-websocket": "^4.13.0",
|
|
32
|
+
"streamplace": "0.6.37",
|
|
33
|
+
"zustand": "^5.0.5"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": "*"
|
|
8
37
|
},
|
|
9
|
-
"
|
|
10
|
-
"author": "",
|
|
11
|
-
"license": "ISC",
|
|
12
|
-
"packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab"
|
|
38
|
+
"gitHead": "87a2627518e509fd364199cb71657a94eac51066"
|
|
13
39
|
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React, { useContext, useRef } from "react";
|
|
2
|
+
import { LivestreamContext, makeLivestreamStore } from "../livestream-store";
|
|
3
|
+
import { useLivestreamWebsocket } from "./websocket";
|
|
4
|
+
|
|
5
|
+
export function LivestreamProvider({
|
|
6
|
+
children,
|
|
7
|
+
src,
|
|
8
|
+
}: {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
src: string;
|
|
11
|
+
}) {
|
|
12
|
+
const context = useContext(LivestreamContext);
|
|
13
|
+
const store = useRef(makeLivestreamStore()).current;
|
|
14
|
+
if (context) {
|
|
15
|
+
// this is ok, there's use cases for having one in another
|
|
16
|
+
// like having a player component that's independently usable
|
|
17
|
+
// but can also be embedded within an entire livestream page
|
|
18
|
+
return <>{children}</>;
|
|
19
|
+
}
|
|
20
|
+
(window as any).livestreamStore = store;
|
|
21
|
+
return (
|
|
22
|
+
<LivestreamContext.Provider value={{ store: store }}>
|
|
23
|
+
<LivestreamPoller src={src}>{children}</LivestreamPoller>
|
|
24
|
+
</LivestreamContext.Provider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function LivestreamPoller({
|
|
29
|
+
children,
|
|
30
|
+
src,
|
|
31
|
+
}: {
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
src: string;
|
|
34
|
+
}) {
|
|
35
|
+
useLivestreamWebsocket(src);
|
|
36
|
+
return <>{children}</>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import useWebSocket from "react-use-websocket";
|
|
3
|
+
import { useHandleWebsocketMessages } from "../livestream-store";
|
|
4
|
+
import { useUrl } from "../streamplace-store";
|
|
5
|
+
|
|
6
|
+
export function useLivestreamWebsocket(src: string) {
|
|
7
|
+
const url = useUrl();
|
|
8
|
+
const handleWebSocketMessages = useHandleWebsocketMessages();
|
|
9
|
+
|
|
10
|
+
let wsUrl = url.replace(/^http\:/, "ws:");
|
|
11
|
+
wsUrl = wsUrl.replace(/^https\:/, "wss:");
|
|
12
|
+
|
|
13
|
+
const ref = useRef<any[]>([]);
|
|
14
|
+
const handle = useRef<NodeJS.Timeout | null>(null);
|
|
15
|
+
|
|
16
|
+
const { readyState } = useWebSocket(`${wsUrl}/api/websocket/${src}`, {
|
|
17
|
+
reconnectInterval: 1000,
|
|
18
|
+
shouldReconnect: () => true,
|
|
19
|
+
|
|
20
|
+
onOpen: () => {
|
|
21
|
+
ref.current = [];
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
onError: (e) => {
|
|
25
|
+
console.log("onError", e);
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// spamming the redux store with messages causes a zillion re-renders,
|
|
29
|
+
// so we batch them up a bit
|
|
30
|
+
onMessage: (msg) => {
|
|
31
|
+
try {
|
|
32
|
+
const data = JSON.parse(msg.data);
|
|
33
|
+
ref.current.push(data);
|
|
34
|
+
if (handle.current) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
handle.current = setTimeout(() => {
|
|
38
|
+
handleWebSocketMessages(ref.current);
|
|
39
|
+
ref.current = [];
|
|
40
|
+
handle.current = null;
|
|
41
|
+
}, 250);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.log("onMessage parse error", e);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { RichText } from "@atproto/api";
|
|
2
|
+
import {
|
|
3
|
+
isLink,
|
|
4
|
+
isMention,
|
|
5
|
+
} from "@atproto/api/dist/client/types/app/bsky/richtext/facet";
|
|
6
|
+
import {
|
|
7
|
+
ChatMessageViewHydrated,
|
|
8
|
+
PlaceStreamChatMessage,
|
|
9
|
+
PlaceStreamDefs,
|
|
10
|
+
} from "streamplace";
|
|
11
|
+
import { useChatProfile, useDID, useHandle } from "../streamplace-store";
|
|
12
|
+
import { usePDSAgent } from "../streamplace-store/xrpc";
|
|
13
|
+
import { LivestreamState } from "./livestream-state";
|
|
14
|
+
import { getStoreFromContext, useLivestreamStore } from "./livestream-store";
|
|
15
|
+
|
|
16
|
+
export const useReplyToMessage = () =>
|
|
17
|
+
useLivestreamStore((state) => state.replyToMessage);
|
|
18
|
+
|
|
19
|
+
export const useSetReplyToMessage = () => {
|
|
20
|
+
const store = getStoreFromContext();
|
|
21
|
+
return (message: ChatMessageViewHydrated | null) => {
|
|
22
|
+
store.setState({ replyToMessage: message });
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type NewChatMessage = {
|
|
27
|
+
text: string;
|
|
28
|
+
reply?: {
|
|
29
|
+
cid: string;
|
|
30
|
+
uri: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const useCreateChatMessage = () => {
|
|
35
|
+
const pdsAgent = usePDSAgent();
|
|
36
|
+
const store = getStoreFromContext();
|
|
37
|
+
const userDID = useDID();
|
|
38
|
+
const userHandle = useHandle();
|
|
39
|
+
const chatProfile = useChatProfile();
|
|
40
|
+
|
|
41
|
+
return async (msg: NewChatMessage) => {
|
|
42
|
+
if (!pdsAgent || !userDID) {
|
|
43
|
+
throw new Error("No PDS agent or user DID found");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let state = store.getState();
|
|
47
|
+
|
|
48
|
+
const streamerProfile = state.profile;
|
|
49
|
+
|
|
50
|
+
if (!streamerProfile) {
|
|
51
|
+
throw new Error("Profile not found");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rt = new RichText({ text: msg.text });
|
|
55
|
+
rt.detectFacetsWithoutResolution();
|
|
56
|
+
|
|
57
|
+
const record: PlaceStreamChatMessage.Record = {
|
|
58
|
+
text: msg.text,
|
|
59
|
+
createdAt: new Date().toISOString(),
|
|
60
|
+
streamer: streamerProfile.did,
|
|
61
|
+
...(msg.reply
|
|
62
|
+
? {
|
|
63
|
+
reply: {
|
|
64
|
+
root: {
|
|
65
|
+
cid: msg.reply.cid,
|
|
66
|
+
uri: msg.reply.uri,
|
|
67
|
+
},
|
|
68
|
+
parent: {
|
|
69
|
+
cid: msg.reply.cid,
|
|
70
|
+
uri: msg.reply.uri,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
: {}),
|
|
75
|
+
...(rt.facets && rt.facets.length > 0
|
|
76
|
+
? {
|
|
77
|
+
facets: rt.facets.map((facet) => ({
|
|
78
|
+
index: facet.index,
|
|
79
|
+
features: facet.features
|
|
80
|
+
.filter(
|
|
81
|
+
(feature) =>
|
|
82
|
+
feature.$type === "app.bsky.richtext.facet#link" ||
|
|
83
|
+
feature.$type === "app.bsky.richtext.facet#mention",
|
|
84
|
+
)
|
|
85
|
+
.map((feature) => {
|
|
86
|
+
if (isLink(feature)) {
|
|
87
|
+
return {
|
|
88
|
+
$type: "app.bsky.richtext.facet#link",
|
|
89
|
+
uri: feature.uri,
|
|
90
|
+
};
|
|
91
|
+
} else if (isMention(feature)) {
|
|
92
|
+
return {
|
|
93
|
+
$type: "app.bsky.richtext.facet#mention",
|
|
94
|
+
did: feature.did,
|
|
95
|
+
};
|
|
96
|
+
} else {
|
|
97
|
+
throw new Error("invalid code path");
|
|
98
|
+
}
|
|
99
|
+
}),
|
|
100
|
+
})),
|
|
101
|
+
}
|
|
102
|
+
: {}),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const localChat: ChatMessageViewHydrated = {
|
|
106
|
+
uri: `local-${Date.now()}`,
|
|
107
|
+
cid: "",
|
|
108
|
+
author: {
|
|
109
|
+
did: userDID,
|
|
110
|
+
handle: userHandle || userDID,
|
|
111
|
+
},
|
|
112
|
+
record: record,
|
|
113
|
+
indexedAt: new Date().toISOString(),
|
|
114
|
+
chatProfile: chatProfile || undefined,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
state = reduceChat(state, [localChat], []);
|
|
118
|
+
store.setState(state);
|
|
119
|
+
|
|
120
|
+
await pdsAgent.com.atproto.repo.createRecord({
|
|
121
|
+
repo: userDID,
|
|
122
|
+
collection: "place.stream.chat.message",
|
|
123
|
+
record,
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const CHAT_LIMIT = 20;
|
|
129
|
+
|
|
130
|
+
export const reduceChat = (
|
|
131
|
+
state: LivestreamState,
|
|
132
|
+
messages: ChatMessageViewHydrated[],
|
|
133
|
+
blocks: PlaceStreamDefs.BlockView[],
|
|
134
|
+
): LivestreamState => {
|
|
135
|
+
state = { ...state } as LivestreamState;
|
|
136
|
+
let newChat: { [key: string]: ChatMessageViewHydrated } = {
|
|
137
|
+
...state.chatIndex,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Add new messages
|
|
141
|
+
for (let message of messages) {
|
|
142
|
+
const date = new Date(message.record.createdAt);
|
|
143
|
+
const key = `${date.getTime()}-${message.uri}`;
|
|
144
|
+
|
|
145
|
+
// Remove existing local message matching the server one
|
|
146
|
+
if (!message.uri.startsWith("local-")) {
|
|
147
|
+
const existingLocalMessageKey = Object.keys(newChat).find((k) => {
|
|
148
|
+
const msg = newChat[k];
|
|
149
|
+
return (
|
|
150
|
+
msg.uri.startsWith("local-") &&
|
|
151
|
+
msg.record.text === message.record.text &&
|
|
152
|
+
msg.author.did === message.author.did
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (existingLocalMessageKey) {
|
|
157
|
+
delete newChat[existingLocalMessageKey];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Handle reply information for local-first messages
|
|
162
|
+
if (message.record.reply) {
|
|
163
|
+
const reply = message.record.reply as {
|
|
164
|
+
parent?: { uri: string; cid: string };
|
|
165
|
+
root?: { uri: string; cid: string };
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const parentUri = reply?.parent?.uri || reply?.root?.uri;
|
|
169
|
+
|
|
170
|
+
if (parentUri) {
|
|
171
|
+
// First try to find the parent message in our chat
|
|
172
|
+
const parentMsgKey = Object.keys(newChat).find(
|
|
173
|
+
(k) => newChat[k].uri === parentUri,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (parentMsgKey) {
|
|
177
|
+
// Found the parent message, add its info to our message
|
|
178
|
+
const parentMsg = newChat[parentMsgKey];
|
|
179
|
+
message = {
|
|
180
|
+
...message,
|
|
181
|
+
replyTo: {
|
|
182
|
+
cid: parentMsg.cid,
|
|
183
|
+
uri: parentMsg.uri,
|
|
184
|
+
author: parentMsg.author,
|
|
185
|
+
record: parentMsg.record,
|
|
186
|
+
chatProfile: parentMsg.chatProfile,
|
|
187
|
+
indexedAt: parentMsg.indexedAt,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
newChat[key] = message;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const block of blocks) {
|
|
198
|
+
for (const [k, v] of Object.entries(newChat)) {
|
|
199
|
+
if (v.author.did === block.record.subject) {
|
|
200
|
+
delete newChat[k];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let newChatList = Object.values(newChat).sort((a, b) =>
|
|
206
|
+
new Date(a.record.createdAt) > new Date(b.record.createdAt) ? 1 : -1,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
newChatList = newChatList.slice(-CHAT_LIMIT);
|
|
210
|
+
|
|
211
|
+
newChat = newChatList.reduce(
|
|
212
|
+
(acc, msg) => {
|
|
213
|
+
acc[msg.uri] = msg;
|
|
214
|
+
return acc;
|
|
215
|
+
},
|
|
216
|
+
{} as { [key: string]: ChatMessageViewHydrated },
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
...state,
|
|
221
|
+
chatIndex: newChat,
|
|
222
|
+
chat: newChatList,
|
|
223
|
+
};
|
|
224
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
import { LivestreamStore } from "../livestream-store/livestream-store";
|
|
3
|
+
|
|
4
|
+
type LivestreamContextType = {
|
|
5
|
+
store: LivestreamStore;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const LivestreamContext = createContext<LivestreamContextType | null>(
|
|
9
|
+
null,
|
|
10
|
+
);
|